feat(cli)!: introduce standalone `rnx-cli` (#3244)

This commit is contained in:
Tommy Nguyen 2024-08-13 18:30:17 +02:00 коммит произвёл GitHub
Родитель d21adb8957
Коммит 96a34d4794
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
26 изменённых файлов: 656 добавлений и 245 удалений

Просмотреть файл

@ -0,0 +1,5 @@
---
"@rnx-kit/cli": minor
---
Introduce standalone `rnx-cli`. The minimum required Node version has also been bumped to 16.17.

Просмотреть файл

@ -5,26 +5,33 @@
Command-line interface for working with packages in your repo.
## Bundle a Package
> [!NOTE]
>
> All commands below are also a plugin to `@react-native-community/cli`, meaning
> they will work with both `react-native` and `rnc-cli` commands. Just make sure
> to prefix the command with `rnx-` e.g., `rnx-cli start` becomes
> `react-native rnx-start`. The prefix is to avoid name clashes.
Bundle a package using [Metro](https://facebook.github.io/metro). The bundling
process uses optional
[configuration](https://github.com/microsoft/rnx-kit/tree/main/packages/config)
parameters and command-line overrides.
## `rnx-cli bundle`
The command `react-native rnx-bundle` is meant to be a drop-in replacement for
`react-native bundle`. If `rnx-bundle` ever becomes widely accepted, we will
work on upstreaming it to `@react-native-community/cli`, along with supporting
libraries for package configuration and Metro plugins.
Bundle a package using [Metro][]. The bundling process uses optional
[configuration][] parameters and command-line overrides.
### Example Commands
> [!NOTE]
>
> This command is meant to be a drop-in replacement for `react-native bundle`.
> If `rnx-bundle` ever becomes widely accepted, we will work on upstreaming it
> to `@react-native-community/cli`, along with supporting libraries for package
> configuration and Metro plugins.
### Example Usages
```sh
yarn react-native rnx-bundle
yarn rnx-cli bundle
```
```sh
yarn react-native rnx-bundle \
yarn rnx-cli bundle \
--entry-file src/index.ts \
--bundle-output main.jsbundle \
--platform ios \
@ -48,7 +55,7 @@ yarn react-native rnx-bundle \
],
"@rnx-kit/metro-plugin-typescript"
],
"targets": ["ios", "android", "windows", "macos"],
"targets": ["android", "ios", "macos", "windows"],
"platforms": {
"android": {
"assetsDest": "dist/res"
@ -86,44 +93,94 @@ dependencies.
### Command-Line Overrides
| Override | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --id [id] | Target bundle definition. This is only needed when the rnx-kit configuration has multiple bundle definitions. |
| --entry-file [file] | Path to the root JavaScript or TypeScript file, either absolute or relative to the package. |
| --platform [`ios` \| `android` \| `windows` \| `win32` \| `macos`] | Target platform. When not given, all platforms in the rnx-kit configuration are bundled. |
| --dev [boolean] | If false, warnings are disabled and the bundle is minified (default: true). |
| --minify [boolean] | Controls whether or not the bundle is minified. Disabling minification is useful for test builds. |
| --bundle-output [path] | Path to the output bundle file, either absolute or relative to the package. |
| --bundle-encoding [`utf8` \| `utf16le` \| `ascii`] | [Character encoding](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) to use when writing the bundle file. |
| --max-workers [number] | Specifies the maximum number of parallel worker threads to use for transforming files. This defaults to the number of cores available on your machine. |
| --sourcemap-output [string] | Path where the bundle source map is written, either absolute or relative to the package. |
| --sourcemap-sources-root [string] | Path to use when relativizing file entries in the bundle source map. |
| --assets-dest [path] | Path where bundle assets like images are written, either absolute or relative to the package. If not given, assets are ignored. |
| --tree-shake [boolean] | Enable tree shaking to remove unused code and reduce the bundle size. |
| --reset-cache | Reset the Metro cache. |
| --config [string] | Path to the Metro configuration file. |
| -h, --help | Show usage information. |
<!-- @rnx-kit/cli/bundle start -->
## Start a Bundle Server
| Option | Description |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| --id &lt;id&gt; | Target bundle definition; only needed when the rnx-kit configuration has multiple bundle definitions |
| --entry-file &lt;path&gt; | Path to the root JavaScript or TypeScript file, either absolute or relative to the package |
| --platform &lt;ios&verbar;android&verbar;windows&verbar;win32&verbar;macos&gt; | Target platform; when unspecified, all platforms in the rnx-kit configuration are bundled |
| --dev [boolean] | If false, warnings are disabled and the bundle is minified |
| --minify [boolean] | Controls whether or not the bundle is minified (useful for test builds) |
| --bundle-output &lt;string&gt; | Path to the output bundle file, either absolute or relative to the package |
| --bundle-encoding &lt;utf8&verbar;utf16le&verbar;ascii&gt; | Character encoding to use when writing the bundle file |
| --max-workers &lt;number&gt; | Specifies the maximum number of parallel worker threads to use for transforming files; defaults to the number of cores available on your machine |
| --sourcemap-output &lt;string&gt; | Path where the bundle source map is written, either absolute or relative to the package |
| --sourcemap-sources-root &lt;string&gt; | Path to use when relativizing file entries in the bundle source map |
| --sourcemap-use-absolute-path | Report SourceMapURL using its full path |
| --assets-dest &lt;path&gt; | Path where bundle assets like images are written, either absolute or relative to the package; if unspecified, assets are ignored |
| --unstable-transform-profile &lt;string&gt; | [Experimental] Transform JS for a specific JS engine; currently supported: hermes, hermes-canary, default |
| --reset-cache | Reset the Metro cache |
| --config &lt;string&gt; | Path to the Metro configuration file |
| --tree-shake [boolean] | Enable tree shaking to remove unused code and reduce the bundle size |
Start a bundle server for a package using
[Metro](https://facebook.github.io/metro). The bundle server uses optional
[configuration](https://github.com/microsoft/rnx-kit/tree/main/packages/config)
parameters and command-line overrides.
<!-- @rnx-kit/cli/bundle end -->
The command `react-native rnx-start` is meant to be a drop-in replacement for
`react-native start`. If `rnx-start` ever becomes widely accepted, we will work
on upstreaming it to `@react-native-community/cli`, along with supporting
libraries for package configuration and Metro plugins.
## `rnx-cli config`
Routes to
[`react-native config`](https://github.com/react-native-community/cli/blob/main/packages/cli-config#readme).
## `rnx-cli doctor`
Routes to
[`react-native doctor`](https://github.com/react-native-community/cli/blob/main/packages/cli-doctor#readme).
## `rnx-cli info`
Routes to
[`react-native info`](https://github.com/react-native-community/cli/blob/main/packages/cli-doctor#info).
## `rnx-cli build-android`
Routes to
[`react-native build-android`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-android#build-android).
## `rnx-cli build-ios`
Routes to
[`react-native build-ios`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-ios#build-ios).
## `rnx-cli log-android`
Routes to
[`react-native log-android`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-android#log-android).
## `rnx-cli log-ios`
Routes to
[`react-native log-ios`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-ios#log-ios).
## `rnx-cli run-android`
Routes to
[`react-native run-android`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-android#run-android).
## `rnx-cli run-ios`
Routes to
[`react-native run-ios`](https://github.com/react-native-community/cli/blob/main/packages/cli-platform-ios#run-ios).
## `rnx-cli start`
Start a bundle server for a package using [Metro][]. The bundle server uses
optional [configuration][] parameters and command-line overrides.
> [!NOTE]
>
> This command is meant to be a drop-in replacement for `react-native start`. If
> `rnx-start` ever becomes widely accepted, we will work on upstreaming it to
> `@react-native-community/cli`, along with supporting libraries for package
> configuration and Metro plugins.
### Example Commands
```sh
yarn react-native rnx-start
yarn rnx-cli start
```
```sh
yarn react-native rnx-start --host localhost --port 8812
yarn rnx-cli start --host 127.0.0.1 --port 8812
```
### Example Configuration
@ -156,74 +213,91 @@ from the bundle configuration (or its [defaults](#bundle-defaults)).
### Command-Line Overrides
| Override | Description |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --port [number] | Host port to use when listening for incoming server requests. |
| --host [string] | Host name or address to bind when listening for incoming server requests. When not given, requests from all addresses are accepted. |
| --projectRoot [path] | Path to the root of your react-native project. The bundle server uses this root path to resolve all web requests. |
| --watchFolders [paths] | Additional folders which will be added to the file-watch list. Comma-separated. By default, Metro watches all project files. |
| --assetPlugins [list] | Additional asset plugins to be used by the Metro Babel transformer. Comma-separated list containing plugin module names or absolute paths to plugin packages. |
| --sourceExts [list] | Additional source-file extensions to include when generating bundles. Comma-separated list, excluding the leading dot. |
| --max-workers [number] | Specifies the maximum number of parallel worker threads to use for transforming files. This defaults to the number of cores available on your machine. |
| --reset-cache | Reset the Metro cache. |
| --custom-log-reporter-path [string] | Path to a JavaScript file which exports a Metro `TerminalReporter` function. This replaces the default reporter, which writes all messages to the Metro console. |
| --https | Use a secure (https) web server. When not specified, an insecure (http) web server is used. |
| --key [path] | Path to a custom SSL private key file to use for secure (https) communication. |
| --cert [path] | Path to a custom SSL certificate file to use for secure (https) communication. |
| --config [string] | Path to the Metro configuration file. |
| --no-interactive | Disables interactive mode. |
| --id | Specify which bundle configuration to use if server configuration is missing. |
<!-- @rnx-kit/cli/start start -->
## Manage Dependencies
| Option | Description |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --port &lt;number&gt; | Host port to use when listening for incoming server requests |
| --host &lt;string&gt; | Host name or address to bind when listening for incoming server requests; when not specified, requests from all addresses are accepted |
| --project-root &lt;path&gt; | Path to the root of your react-native project; the bundle server uses this path to resolve all web requests |
| --watch-folders &lt;paths&gt; | Additional folders which will be added to the watched files list, comma-separated; by default, Metro watches all project files |
| --asset-plugins &lt;list&gt; | Additional asset plugins to be used by Metro's Babel transformer; comma-separated list containing plugin module names or absolute paths to plugin packages |
| --source-exts &lt;list&gt; | Additional source file extensions to include when generating bundles; comma-separated list, excluding the leading dot |
| --max-workers &lt;number&gt; | Specifies the maximum number of parallel worker threads to use for transforming files; defaults to the number of cores available on your machine |
| --reset-cache | Reset the Metro cache |
| --custom-log-reporter-path &lt;string&gt; | Path to a JavaScript file which exports a Metro 'TerminalReporter' function; replaces the default reporter that writes all messages to the Metro console |
| --https | Use a secure (https) web server; when not specified, an insecure (http) web server is used |
| --key &lt;path&gt; | Path to a custom SSL private key file to use for secure (https) communication |
| --cert &lt;path&gt; | Path to a custom SSL certificate file to use for secure (https) communication |
| --config &lt;string&gt; | Path to the Metro configuration file |
| --no-interactive | Disables interactive mode |
| --id &lt;string&gt; | Specify which bundle configuration to use if server configuration is missing |
<!-- @rnx-kit/cli/start end -->
## `rnx-cli align-deps`
Manage dependencies within a repository and across many repositories.
```
$ yarn react-native rnx-align-deps [options] [/path/to/package.json]
```sh
yarn rnx-cli align-deps [options] [/path/to/package.json]
```
Refer to
[@rnx-kit/align-deps](https://github.com/microsoft/rnx-kit/tree/main/packages/align-deps)
for details.
Refer to [@rnx-kit/align-deps][] for details.
## Generate a Third-Party Notice for a Package
Generate a 3rd-party notice, which is an aggregation of all the LICENSE files
from your package's dependencies.
> NOTE: A 3rd-party notice is a **legal document**. You are solely responsble
> for its content, even if you use this command to assist you in generating it.
> You should consult with an attorney to ensure your notice meets all legal
> requirements.
```
$ yarn react-native rnx-write-third-party-notices [options]
```
| Option | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| --root-path [path] | The root of the repo. This is the starting point for finding each module in the source map dependency graph. |
| --source-map-file [file] | The source map file associated with the package's entry file. This source map eventually leads to all package dependencies and their licenses. |
| --json | Format the 3rd-party notice file as JSON instead of text. |
| --output-file [file] | The path to use when writing the 3rd-party notice file. |
| --ignore-scopes [string] | Comma-separated list of `npm` scopes to ignore when traversing the source map dependency graph. |
| --ignore-modules [string] | Comma-separated list of modules to ignore when traversing the source map dependency graph. |
| --preamble-text [string] | A string to prepend to the start of the 3rd-party notice. |
| --additional-text [path] | A string to append to the end of the 3rd-party notice. |
## Clean a React Native Project
> Deprecated: This command was upstreamed to `@react-native-community/cli`. As
> of [v8.0](https://github.com/react-native-community/cli/releases/tag/v8.0.0),
> you can use `react-native clean` instead.
## `rnx-cli clean`
Cleans your project by removing React Native related caches and modules.
```
$ yarn react-native rnx-clean [options]
```sh
yarn rnx-cli clean [options]
```
| Option | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --include [string] | Comma-separated flag of caches to clear e.g npm,yarn . When not specified , only non-platform specific caches are cleared. [android,cocoapods,npm,metro,watchman,yarn] |
| --project-root [path] | Root path to your React Native project. When not specified, defaults to current working directory |
<!-- @rnx-kit/cli/clean start -->
| Option | Description |
| ----------------------------------------------------------- | -------------------------------------------------------- |
| --include &lt;android,cocoapods,metro,npm,watchman,yarn&gt; | Comma-separated flag of caches to clear e.g., `npm,yarn` |
| --project-root &lt;path&gt; | Root path to your React Native project |
| --verify-cache | Whether to verify the integrity of the cache |
<!-- @rnx-kit/cli/clean end -->
## `rnx-cli write-third-party-notices`
Generate a third-party notice, an aggregation of all the LICENSE files from your
package's dependencies.
> [!NOTE]
>
> A third-party notice is a **legal document**. You are solely responsble for
> its content, even if you use this command to assist you in generating it. You
> should consult with an attorney to ensure your notice meets all legal
> requirements.
```sh
yarn rnx-cli write-third-party-notices [options]
```
<!-- @rnx-kit/cli/write-third-party-notices start -->
| Option | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| --root-path &lt;path&gt; | The root of the repo — the starting point for finding each module in the source map dependency graph |
| --source-map-file &lt;file&gt; | The source map file associated with the package's entry file — this source map eventually leads to all package dependencies and their licenses |
| --json | Format the 3rd-party notice file as JSON instead of text |
| --output-file &lt;file&gt; | The path to use when writing the 3rd-party notice file |
| --ignore-scopes &lt;string&gt; | Comma-separated list of npm scopes to ignore when traversing the source map dependency graph |
| --ignore-modules &lt;string&gt; | Comma-separated list of modules to ignore when traversing the source map dependency graph |
| --preamble-text &lt;string&gt; | A string to prepend to the start of the 3rd-party notice |
| --additional-text &lt;string&gt; | A string to append to the end of the 3rd-party notice |
<!-- @rnx-kit/cli/write-third-party-notices end -->
<!-- References -->
[@rnx-kit/align-deps]:
https://github.com/microsoft/rnx-kit/tree/main/packages/align-deps#readme
[Metro]: https://facebook.github.io/metro
[configuration]:
https://github.com/microsoft/rnx-kit/tree/main/packages/config#readme

2
packages/cli/bin/rnx-cli.cjs Executable file
Просмотреть файл

@ -0,0 +1,2 @@
#!/usr/bin/env node
require("../lib/bin/rnx-cli.js").main();

Просмотреть файл

@ -9,11 +9,15 @@
"email": "microsoftopensource@users.noreply.github.com"
},
"files": [
"bin/rnx-cli.mjs",
"lib/**/*.d.ts",
"lib/**/*.js",
"react-native.config.js"
],
"main": "lib/index.js",
"bin": {
"rnx-cli": "bin/rnx-cli.cjs"
},
"types": "lib/index.d.ts",
"exports": {
".": {
@ -49,11 +53,9 @@
"@rnx-kit/tools-language": "^2.0.0",
"@rnx-kit/tools-node": "^2.1.1",
"@rnx-kit/tools-react-native": "^1.4.0",
"fs-extra": "^10.0.0",
"node-fetch": "^2.6.7",
"commander": "^11.1.0",
"ora": "^5.4.1",
"qrcode": "^1.5.0",
"readline": "^1.3.0"
"qrcode": "^1.5.0"
},
"peerDependencies": {
"jest": ">=26.0",
@ -74,30 +76,31 @@
"@rnx-kit/eslint-config": "*",
"@rnx-kit/jest-preset": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tools-filesystem": "*",
"@rnx-kit/tsconfig": "*",
"@types/connect": "^3.4.36",
"@types/fs-extra": "^9.0.0",
"@types/jest": "^29.2.1",
"@types/node": "^20.0.0",
"@types/node-fetch": "^2.6.5",
"@types/qrcode": "^1.4.2",
"eslint": "^8.56.0",
"jest": "^29.2.1",
"memfs": "^4.0.0",
"markdown-table": "^3.0.0",
"metro": "^0.80.3",
"metro-babel-transformer": "^0.80.0",
"metro-config": "^0.80.3",
"prettier": "^3.0.0",
"react": "18.2.0",
"react-native": "^0.74.0",
"tsx": "^4.15.0",
"type-fest": "^4.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=16.17"
},
"depcheck": {
"ignoreMatches": [
"connect",
"jest-cli",
"readline"
"connect"
]
},
"jest": {

Просмотреть файл

@ -0,0 +1,41 @@
#!/usr/bin/env node --import tsx
import { markdownTable } from "markdown-table";
import * as fs from "node:fs";
import { reactNativeConfig } from "../src/index.js";
const README = "README.md";
const UTF8 = { encoding: "utf-8" as const };
const readme = fs.readFileSync(README, UTF8);
const updatedReadme = reactNativeConfig.commands.reduce(
(readme, { name, options }) => {
if (!options) {
return readme;
}
const table = markdownTable([
["Option", "Description"],
...options.map(({ name, description }) => {
const flag = name
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("|", "&verbar;");
return [flag, description];
}),
]);
const id = name.replace("rnx-", "@rnx-kit/cli/");
const tokenStart = `<!-- ${id} start -->`;
const tokenEnd = `<!-- ${id} end -->`;
return readme.replace(
new RegExp(`${tokenStart}([^]+)${tokenEnd}`),
`${tokenStart}\n\n${table}\n\n${tokenEnd}`
);
},
readme
);
if (updatedReadme !== readme) {
fs.writeFileSync(README, updatedReadme);
}

Просмотреть файл

@ -0,0 +1,116 @@
import type {
Command as BaseCommand,
Config as BaseConfig,
} from "@react-native-community/cli-types";
import {
findPackageDependencyDir,
readPackage,
} from "@rnx-kit/tools-node/package";
import {
getCurrentState,
getSavedState,
saveConfigToCache,
} from "@rnx-kit/tools-react-native/cache";
import { resolveCommunityCLI } from "@rnx-kit/tools-react-native/context";
import { reactNativeConfig } from "../index";
type Command = BaseCommand<false> | BaseCommand<true>;
type Commands = Record<string, Command>;
type Config = BaseConfig & { __rnxFastPath?: true; commands: Command[] };
const RNX_PREFIX = "rnx-";
function findReactNativePath(root: string, resolveSymlinks = false) {
const dir = findPackageDependencyDir("react-native", {
startDir: root,
resolveSymlinks,
});
if (!dir) {
throw new Error("Unable to resolve module 'react-native'");
}
return dir;
}
export function getCoreCommands() {
const start = RNX_PREFIX.length;
return reactNativeConfig.commands.map((command) => ({
...command,
name: command.name.substring(start),
}));
}
export function uniquify(commands: Command[]): Command[] {
const uniqueCommands: Commands = {};
for (const command of commands) {
const { name } = command;
if (name.startsWith(RNX_PREFIX)) {
command.name = name.substring(RNX_PREFIX.length);
uniqueCommands[command.name] = command;
} else if (!uniqueCommands[name]) {
uniqueCommands[name] = command;
}
}
return Object.values(uniqueCommands);
}
export function loadContext(userCommand: string, root = process.cwd()): Config {
// The fast path avoids traversing project dependencies because we know what
// information our commands depend on.
const coreCommands = getCoreCommands();
const useFastPath = coreCommands.some(({ name }) => name === userCommand);
if (useFastPath) {
let reactNativePath: string;
let reactNativeVersion: string;
return {
__rnxFastPath: true,
root,
get reactNativePath() {
if (!reactNativePath) {
reactNativePath = findReactNativePath(root);
}
return reactNativePath;
},
get reactNativeVersion() {
if (!reactNativeVersion) {
const reactNativePath = findReactNativePath(root);
const { version } = readPackage(reactNativePath);
reactNativeVersion = version;
}
return reactNativeVersion;
},
get dependencies(): Config["dependencies"] {
throw new Error("Unexpected access to `dependencies`");
},
commands: coreCommands,
get healthChecks(): Config["healthChecks"] {
throw new Error("Unexpected access to `healthChecks`");
},
get platforms(): Config["platforms"] {
throw new Error("Unexpected access to `platforms`");
},
get project(): Config["project"] {
throw new Error("Unexpected access to `project`");
},
};
}
const rncli = resolveCommunityCLI(root);
const { loadConfig } = require(rncli);
const config =
loadConfig.length === 1
? loadConfig({ projectRoot: root })
: loadConfig(root);
// We will always load from disk because functions cannot be serialized.
// However, we should refresh the cache if needed.
const state = getCurrentState(root);
if (state !== getSavedState(root)) {
saveConfigToCache(root, state, config);
}
// Before returning the config, remove the `rnx-` prefix from our commands,
// and ensure commands are unique.
config.commands = uniquify(config.commands);
return config;
}

Просмотреть файл

@ -0,0 +1,37 @@
import type { Command, Config } from "@react-native-community/cli-types";
import { resolveCommunityCLI } from "@rnx-kit/tools-react-native/context";
function tryImport(module: string, fromDir: string) {
try {
const p = require.resolve(module, { paths: [fromDir] });
return require(p);
} catch (_) {
return undefined;
}
}
export function findExternalCommands(config: Config): Command[] {
if ("__rnxFastPath" in config) {
// Fast path means we don't need to do anything here
return [];
}
const externalCommands: Command[] = [
{
name: "config",
description:
"Prints the configuration for the project and its dependencies in JSON format; used by autolinking",
func: () => console.log(JSON.stringify(config, undefined, 2)),
},
];
const rncli = resolveCommunityCLI(config.root);
const cliDoctor = tryImport("@react-native-community/cli-doctor", rncli);
if (cliDoctor?.commands) {
const commands = Object.values(cliDoctor.commands) as Command[];
externalCommands.push(...commands);
}
return externalCommands;
}

Просмотреть файл

@ -0,0 +1,38 @@
import { Command } from "commander";
import * as path from "node:path";
import { loadContext } from "./context";
import { findExternalCommands } from "./externalCommands";
export function main() {
const [, , userCommand] = process.argv;
const context = loadContext(userCommand);
const allCommands = context.commands.concat(findExternalCommands(context));
const program = new Command(path.basename(__filename, ".js"));
for (const {
name,
description,
detached,
options = [],
func,
} of allCommands) {
const command = program.command(name).description(description ?? name);
if (detached) {
command.action((args, command) => func(command.args, args, context));
} else {
command.action((args, command) => func(command.args, context, args));
}
for (const { name, description, parse, default: def } of options) {
if (parse) {
command.option(name, description ?? name, (input) => parse(input), def);
} else {
command.option(name, description, def?.toString());
}
}
}
program.parse();
}

Просмотреть файл

@ -3,10 +3,10 @@ import type { HermesOptions } from "@rnx-kit/config";
import { error, info } from "@rnx-kit/console";
import { findPackageDependencyDir } from "@rnx-kit/tools-node/package";
import { requireModuleFromMetro } from "@rnx-kit/tools-react-native/metro";
import { spawnSync } from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
function hermesBinaryInDir(hermesc: string): string | null {
switch (os.platform()) {

Просмотреть файл

@ -2,7 +2,7 @@ import { info } from "@rnx-kit/console";
import type { BundleArgs as MetroBundleArgs } from "@rnx-kit/metro-service";
import { bundle } from "@rnx-kit/metro-service";
import type { ConfigT } from "metro-config";
import * as path from "path";
import * as path from "node:path";
import { ensureDir } from "../helpers/filesystem";
import { customizeMetroConfig } from "../helpers/metro-config";
import type { CliPlatformBundleConfig } from "./types";

Просмотреть файл

@ -1,16 +1,16 @@
import type { Config as CLIConfig } from "@react-native-community/cli-types";
import { spawn } from "child_process";
import { existsSync as fileExists } from "fs";
import * as fs from "fs/promises";
import { spawn } from "node:child_process";
import { existsSync as fileExists } from "node:fs";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import ora from "ora";
import * as os from "os";
import * as path from "path";
import { asResolvedPath } from "./helpers/parsers";
type Args = {
include?: string;
projectRoot?: string;
verify?: boolean;
verifyCache?: boolean;
};
type Task = {
@ -101,7 +101,7 @@ export async function rnxClean(
label: "Remove node_modules",
action: () => cleanDir(`${root}/node_modules`),
},
...(cliOptions.verify
...(cliOptions.verifyCache
? [
{
label: "Verify npm cache",
@ -191,7 +191,7 @@ export const rnxCleanCommand = {
parse: asResolvedPath,
},
{
name: "--verify",
name: "--verify-cache",
description: "Whether to verify the integrity of the cache",
default: false,
},

Просмотреть файл

@ -4,7 +4,6 @@ import { keysOf } from "@rnx-kit/tools-language/properties";
import type { PackageManifest } from "@rnx-kit/tools-node/package";
import {
findPackageDependencyDir,
findPackageDir,
readPackage,
} from "@rnx-kit/tools-node/package";
import { findUp } from "@rnx-kit/tools-node/path";
@ -12,9 +11,11 @@ import type { AllPlatforms } from "@rnx-kit/tools-react-native";
import { parsePlatform } from "@rnx-kit/tools-react-native";
import type { SpawnSyncOptions } from "child_process";
import { spawnSync } from "child_process";
import * as fs from "fs-extra";
import * as fs from "fs";
import * as nodefs from "fs";
import * as os from "os";
import * as path from "path";
import { ensureDir } from "./helpers/filesystem";
export type AndroidArchive = {
targetName?: string;
@ -53,6 +54,7 @@ export type Context = {
projectRoot: string;
manifest: PackageManifest;
options: Options;
reactNativePath: string;
};
export type AssetsConfig = {
@ -69,6 +71,14 @@ const defaultAndroidConfig: Required<Required<AndroidArchive>["android"]> = {
kotlinVersion: "1.7.22",
};
function cloneFile(src: string, dest: string) {
return fs.promises.copyFile(src, dest, fs.constants.COPYFILE_FICLONE);
}
function cp_r(source: string, destination: string, fs = nodefs) {
return fs.promises.cp(source, destination, { recursive: true });
}
function ensureOption(options: Options, opt: string, flag = opt) {
if (options[opt] == null) {
error(`Missing required option: --${flag}`);
@ -215,7 +225,7 @@ export async function assembleAarBundle(
};
const outputDir = path.join(context.options.assetsDest, "aar");
fs.ensureDirSync(outputDir);
ensureDir(outputDir);
const dest = path.join(outputDir, `${targetName}-${version}.aar`);
@ -245,11 +255,6 @@ export async function assembleAarBundle(
// Run only one Gradle task at a time
run(gradlew, targets, { cwd: androidProject, stdio: "inherit", env });
} else {
const reactNativePath = findPackageDependencyDir("react-native");
if (!reactNativePath) {
throw new Error("Could not find 'react-native'");
}
const buildDir = path.join(
process.cwd(),
"node_modules",
@ -272,7 +277,7 @@ export async function assembleAarBundle(
android?.kotlinVersion ?? defaultAndroidConfig.kotlinVersion;
const buildRelativeReactNativePath = path.relative(
buildDir,
reactNativePath
context.reactNativePath
);
const buildGradle = [
@ -319,7 +324,7 @@ export async function assembleAarBundle(
"",
].join("\n");
fs.ensureDirSync(buildDir);
ensureDir(buildDir);
fs.writeFileSync(path.join(buildDir, "build.gradle"), buildGradle);
fs.writeFileSync(
path.join(buildDir, "gradle.properties"),
@ -331,32 +336,37 @@ export async function assembleAarBundle(
run(gradlew, targets, { cwd: buildDir, stdio: "inherit", env });
}
await Promise.all(targetsToCopy.map(([src, dest]) => fs.copy(src, dest)));
await Promise.all(targetsToCopy.map(([src, dest]) => cloneFile(src, dest)));
}
function copyFiles(files: unknown, destination: string): Promise<void>[] {
function copyFiles(
files: unknown,
destination: string,
fs = nodefs
): Promise<void>[] {
if (!Array.isArray(files) || files.length === 0) {
return [];
}
fs.ensureDirSync(destination);
ensureDir(destination, fs);
return files.map((file) => {
return fs.copy(file, `${destination}/${path.basename(file)}`);
return cp_r(file, `${destination}/${path.basename(file)}`, fs);
});
}
export async function copyAssets(
{ options: { assetsDest, xcassetsDest } }: Context,
packageName: string,
{ assets, strings, xcassets }: NativeAssets
{ assets, strings, xcassets }: NativeAssets,
fs = nodefs
): Promise<void> {
const tasks = [
...copyFiles(assets, `${assetsDest}/assets/${packageName}`),
...copyFiles(strings, `${assetsDest}/strings/${packageName}`),
...copyFiles(assets, `${assetsDest}/assets/${packageName}`, fs),
...copyFiles(strings, `${assetsDest}/strings/${packageName}`, fs),
];
if (typeof xcassetsDest === "string") {
tasks.push(...copyFiles(xcassets, xcassetsDest));
tasks.push(...copyFiles(xcassets, xcassetsDest, fs));
}
await Promise.all(tasks);
@ -460,10 +470,12 @@ export async function gatherConfigs({
*
* @param options Options dictate what gets copied where
*/
export async function copyProjectAssets(options: Options): Promise<void> {
const projectRoot = findPackageDir() || process.cwd();
export async function copyProjectAssets(
options: Options,
{ root: projectRoot, reactNativePath }: CLIConfig
): Promise<void> {
const manifest = readPackage(projectRoot);
const context = { projectRoot, manifest, options };
const context = { projectRoot, manifest, options, reactNativePath };
const assetConfigs = await gatherConfigs(context);
if (!assetConfigs) {
return;
@ -505,7 +517,7 @@ export async function copyProjectAssets(options: Options): Promise<void> {
(!fs.existsSync(destination) || fs.statSync(destination).isDirectory())
) {
info(`Copying Android Archive of "${dependencyName}"`);
copyTasks.push(fs.copy(output, destination));
copyTasks.push(cloneFile(output, destination));
}
}
await Promise.all(copyTasks);
@ -515,31 +527,31 @@ export async function copyProjectAssets(options: Options): Promise<void> {
export const rnxCopyAssetsCommand = {
name: "rnx-copy-assets",
description:
"Copies additional assets not picked by bundlers into desired directory.",
func: (_argv: string[], _config: CLIConfig, options: Options) => {
"Copies additional assets not picked by bundlers into desired directory",
func: (_argv: string[], config: CLIConfig, options: Options) => {
ensureOption(options, "platform");
ensureOption(options, "assetsDest", "assets-dest");
return copyProjectAssets(options);
return copyProjectAssets(options, config);
},
options: [
{
name: "--platform <string>",
description: "platform to target",
description: "Platform to target",
parse: parsePlatform,
},
{
name: "--assets-dest <string>",
description: "path of the directory to copy assets into",
description: "Path of the directory to copy assets into",
},
{
name: "--bundle-aar <boolean>",
description: "whether to bundle AARs of dependencies",
name: "--bundle-aar [boolean]",
description: "Whether to bundle AARs of dependencies",
default: false,
},
{
name: "--xcassets-dest <string>",
description:
"path of the directory to copy Xcode asset catalogs into. Asset catalogs will only be copied if a destination path is specified.",
"Path of the directory to copy Xcode asset catalogs into; asset catalogs will only be copied if a destination path is specified",
},
],
};

Просмотреть файл

@ -1,4 +1,4 @@
import * as nodefs from "fs";
import * as nodefs from "fs"; // Cannot use `node:fs` because of Jest mocks
export function ensureDir(p: string, fs = nodefs): void {
fs.mkdirSync(p, { recursive: true, mode: 0o755 });

Просмотреть файл

@ -1,5 +1,5 @@
import type { TransformProfile } from "metro-babel-transformer";
import * as path from "path";
import * as path from "node:path";
export function asBoolean(value: string): boolean {
switch (value) {

Просмотреть файл

@ -1,3 +1,4 @@
import type { Command } from "@react-native-community/cli-types";
import { rnxAlignDepsCommand } from "./align-deps";
import { rnxBundleCommand } from "./bundle";
import { rnxCleanCommand } from "./clean";
@ -17,7 +18,7 @@ export const reactNativeConfig = {
rnxTestCommand,
rnxWriteThirdPartyNoticesCommand,
rnxCleanCommand,
],
] as Command<false>[],
};
export { rnxAlignDeps, rnxAlignDepsCommand } from "./align-deps";

Просмотреть файл

@ -1,8 +1,7 @@
import { info } from "@rnx-kit/console";
import type { MetroTerminal } from "@rnx-kit/metro-service";
import nodeFetch from "node-fetch";
import readline from "node:readline";
import qrcode from "qrcode";
import readline from "readline";
import type { DevServerMiddleware } from "./types";
type Options = {
@ -44,9 +43,7 @@ export function attachKeyHandlers({
case "j": {
info("Opening debugger...");
// TODO: Remove `node-fetch` when we drop support for Node 16
const ftch = "fetch" in globalThis ? fetch : nodeFetch;
ftch(devServerUrl + "/open-debugger", { method: "POST" });
fetch(devServerUrl + "/open-debugger", { method: "POST" });
break;
}

Просмотреть файл

@ -1,8 +1,8 @@
import type * as logger from "@rnx-kit/console";
import type { Server as Middleware } from "connect";
import type { Server as HttpServer } from "http";
import type { Server as HttpsServer } from "https";
import type { RunServerOptions } from "metro";
import type { Server as HttpServer } from "node:http";
import type { Server as HttpsServer } from "node:https";
// https://github.com/react-native-community/cli/blob/11.x/packages/cli-server-api/src/index.ts#L32
type MiddlewareOptions = {

Просмотреть файл

@ -10,7 +10,7 @@ import {
import type { ReportableEvent, Reporter, RunServerOptions } from "metro";
import type { Middleware } from "metro-config";
import type Server from "metro/src/Server";
import * as path from "path";
import * as path from "node:path";
import { requireExternal } from "./helpers/externals";
import { customizeMetroConfig } from "./helpers/metro-config";
import { asNumber, asResolvedPath, asStringArray } from "./helpers/parsers";

Просмотреть файл

@ -22,7 +22,7 @@ type Options = {
| ((config: CLIConfig) => string | boolean | number);
};
const COMMAND_NAME = "rnx-test";
const COMMAND_NAMES = ["rnx-test", "test"];
const JEST_CLI = ["jest", "jest-cli"];
export function rnxTest(
@ -40,7 +40,9 @@ export function rnxTest(
}
})();
const commandIndex = process.argv.indexOf(COMMAND_NAME);
const commandIndex = process.argv.findIndex((arg) =>
COMMAND_NAMES.includes(arg)
);
if (commandIndex < 0) {
throw new Error("Failed to parse command arguments");
}
@ -50,7 +52,9 @@ export function rnxTest(
const platformIndex = argv.indexOf("--platform");
if (platformIndex < 0) {
throw new Error("A target platform must be specified");
error("A target platform must be specified");
process.exitCode = process.exitCode || 1;
return;
}
// Remove `--platform` otherwise Jest will complain about an unrecognized
@ -124,7 +128,7 @@ export function jestOptions(): Options[] {
}
export const rnxTestCommand = {
name: COMMAND_NAME,
name: COMMAND_NAMES[0],
description: "Test runner for React Native apps",
func: rnxTest,
options: [

Просмотреть файл

@ -1,20 +0,0 @@
"use strict";
const fs = jest.createMockFromModule("fs-extra");
const { vol } = require("memfs");
/** @type {(newMockFiles: { [filename: string]: string }) => void} */
fs.__setMockFiles = (files) => {
vol.reset();
vol.fromJSON(files);
};
fs.__toJSON = () => vol.toJSON();
fs.copy = (...args) => vol.promises.copyFile(...args);
fs.ensureDirSync = (dir) => vol.mkdirSync(dir, { recursive: true });
fs.existsSync = (...args) => vol.existsSync(...args);
fs.writeFileSync = (...args) => vol.writeFileSync(...args);
module.exports = fs;

Просмотреть файл

@ -0,0 +1,66 @@
import type { Command } from "@react-native-community/cli-types";
import { getCoreCommands, loadContext, uniquify } from "../../src/bin/context";
import { reactNativeConfig } from "../../src/index";
jest.mock("@rnx-kit/tools-react-native/context", () => ({
resolveCommunityCLI: () => {
throw new Error("Expected fast path");
},
}));
describe("getCoreCommands()", () => {
it("strips `rnx-` prefix from all commands", () => {
const coreCommands = getCoreCommands();
for (let i = 0; i < coreCommands.length; ++i) {
const modified = coreCommands[i];
const original = reactNativeConfig.commands[i];
expect(modified).not.toBe(original);
expect(modified.name.startsWith("rnx-")).toBe(false);
expect(modified.description).toBe(original.description);
expect(modified.options).toBe(original.options);
expect(modified.func).toBe(original.func);
}
});
});
describe("uniquify()", () => {
function makeCommand(name: string, description: string): Command<false> {
return { name, description } as Command<false>;
}
it("ignores duplicate commands", () => {
const start = makeCommand("start", "first start command");
const start2 = makeCommand("start", "second start command");
expect(uniquify([start, start2])).toMatchObject([start]);
});
it("replaces existing commands with rnx", () => {
const start = makeCommand("start", "original start command");
const rnxBundle = makeCommand("rnx-bundle", "rnx-bundle command");
const rnxStart = makeCommand("rnx-start", "rnx-start command");
expect(uniquify([start, rnxBundle, rnxStart])).toMatchObject([
{ name: "start", description: "rnx-start command" },
{ name: "bundle", description: "rnx-bundle command" },
]);
});
});
describe("loadContext()", () => {
afterAll(() => {
jest.resetAllMocks();
});
it("uses fast code path for rnx commands", () => {
for (const { name } of getCoreCommands()) {
expect(() => loadContext(name)).not.toThrow();
}
});
it("uses full code path for other commands", () => {
expect(() => loadContext("run-android")).toThrow();
expect(() => loadContext("run-ios")).toThrow();
});
});

Просмотреть файл

@ -0,0 +1,29 @@
import type { Config } from "@react-native-community/cli-types";
import { findExternalCommands } from "../../src/bin/externalCommands";
jest.mock("@rnx-kit/tools-react-native/context", () => ({
resolveCommunityCLI: () => "/",
}));
function mockContext(context: unknown = {}): Config {
return context as Config;
}
describe("findExternalCommands()", () => {
afterAll(() => {
jest.resetAllMocks();
});
it("returns immediately on fast path", () => {
const context = mockContext({ __rnxFastPath: true });
expect(findExternalCommands(context)).toEqual([]);
});
it("gracefully handles missing external dependencies", () => {
const commands = findExternalCommands(mockContext());
expect(commands.length).toBe(1);
expect(commands[0].name).toBe("config");
});
});

Просмотреть файл

@ -1,4 +1,4 @@
import * as path from "path";
import * as path from "node:path";
import { assembleAarBundle } from "../../src/copy-assets";
jest.mock("child_process");
@ -7,7 +7,6 @@ jest.unmock("@rnx-kit/console");
describe("assembleAarBundle", () => {
const fs = require("fs");
const fsx = require("fs-extra");
const consoleWarnSpy = jest.spyOn(global.console, "warn");
const spawnSyncSpy = jest.spyOn(require("child_process"), "spawnSync");
@ -25,17 +24,17 @@ describe("assembleAarBundle", () => {
version: "0.0.0-dev",
},
options,
reactNativePath: require.resolve("react-native"),
};
const dummyManifest = JSON.stringify({ version: "0.0.0-dev" });
function findFiles() {
return Object.entries(fsx.__toJSON());
return Object.entries(fs.__toJSON());
}
function mockFiles(files: Record<string, string> = {}) {
fs.__setMockFiles(files);
fsx.__setMockFiles(files);
}
afterEach(() => {

Просмотреть файл

@ -1,4 +1,5 @@
import * as path from "path";
import { mockFS } from "@rnx-kit/tools-filesystem/mocks";
import * as path from "node:path";
import { copyAssets, gatherConfigs, versionOf } from "../../src/copy-assets";
const options = {
@ -15,41 +16,28 @@ const context = {
version: "0.0.0-dev",
},
options,
reactNativePath: require.resolve("react-native"),
};
describe("copyAssets", () => {
const fs = require("fs-extra");
function findFiles() {
return Object.entries(fs.__toJSON());
}
function mockFiles(files: Record<string, string> = {}) {
fs.__setMockFiles(files);
}
afterEach(() => {
mockFiles();
});
afterAll(() => {
jest.clearAllMocks();
});
const mkdirOptions = JSON.stringify({ recursive: true, mode: 0o755 });
test("returns early if there is nothing to copy", async () => {
await copyAssets(context, "test", {});
expect(findFiles()).toEqual([]);
const files = {};
await copyAssets(context, "test", {}, mockFS(files));
expect(Object.keys(files)).toEqual([]);
});
test("copies assets", async () => {
const filename = "arnolds-greatest-movies.md";
const content = "all of them";
mockFiles({ [filename]: content });
const files = { [filename]: content };
await copyAssets(context, "test", { assets: [filename] });
await copyAssets(context, "test", { assets: [filename] }, mockFS(files));
expect(findFiles()).toEqual([
expect(Object.entries(files)).toEqual([
[expect.stringContaining(filename), content],
[expect.stringMatching(`dist[/\\\\]assets[/\\\\]test$`), mkdirOptions],
[
expect.stringMatching(
`dist[/\\\\]assets[/\\\\]test[/\\\\]${filename}$`
@ -62,12 +50,13 @@ describe("copyAssets", () => {
test("copies strings", async () => {
const filename = "arnolds-greatest-lines.md";
const content = "all of them";
mockFiles({ [filename]: content });
const files = { [filename]: content };
await copyAssets(context, "test", { strings: [filename] });
await copyAssets(context, "test", { strings: [filename] }, mockFS(files));
expect(findFiles()).toEqual([
expect(Object.entries(files)).toEqual([
[expect.stringContaining(filename), content],
[expect.stringMatching(`dist[/\\\\]strings[/\\\\]test$`), mkdirOptions],
[
expect.stringMatching(
`dist[/\\\\]strings[/\\\\]test[/\\\\]${filename}$`
@ -80,12 +69,13 @@ describe("copyAssets", () => {
test("copies Xcode asset catalogs", async () => {
const filename = "arnolds-greatest-assets.xcassets";
const content = "all of them";
mockFiles({ [filename]: content });
const files = { [filename]: content };
await copyAssets(context, "test", { xcassets: [filename] });
await copyAssets(context, "test", { xcassets: [filename] }, mockFS(files));
expect(findFiles()).toEqual([
expect(Object.entries(files)).toEqual([
[expect.stringContaining(filename), content],
["xcassets", mkdirOptions],
[expect.stringMatching(`xcassets[/\\\\]${filename}$`), content],
]);
});
@ -93,15 +83,18 @@ describe("copyAssets", () => {
test("does not copy Xcode asset catalogs if destination path is unset", async () => {
const filename = "arnolds-greatest-assets.xcassets";
const content = "all of them";
mockFiles({ [filename]: content });
const files = { [filename]: content };
await copyAssets(
{ ...context, options: { ...options, xcassetsDest: undefined } },
"test",
{ xcassets: [filename] }
{ xcassets: [filename] },
mockFS(files)
);
expect(findFiles()).toEqual([[expect.stringContaining(filename), content]]);
expect(Object.entries(files)).toEqual([
[expect.stringContaining(filename), content],
]);
});
});

Просмотреть файл

@ -3,26 +3,30 @@
"name": "@rnx-kit/test-app",
"version": "0.1.0",
"private": true,
"bin": {
"rnx": "../cli/bin/rnx-cli.cjs",
"rnx.reason": "Workaround for Node not being able to find `rnx-cli` because of Yarn virtual packages"
},
"scripts": {
"build": "react-native rnx-align-deps && rnx-kit-scripts build",
"build:ios": "rnx-kit-scripts build-ios -w SampleCrossApp -s ReactTestApp",
"android": "rnx run-android --no-packager --appId com.msft.identity.client.sample.local",
"build": "rnx align-deps && rnx-kit-scripts build",
"build:android": "rnx-kit-scripts build-android clean build",
"build:ios": "rnx-kit-scripts build-ios -w SampleCrossApp -s ReactTestApp",
"bundle": "rnx bundle",
"bundle+esbuild": "rnx bundle --id esbuild",
"bundle:android": "rnx bundle --platform android",
"bundle:ios": "rnx bundle --platform ios",
"bundle:macos": "rnx bundle --platform macos",
"bundle:windows": "rnx bundle --platform windows",
"depcheck": "rnx-kit-scripts depcheck",
"format": "rnx-kit-scripts format",
"ios": "rnx run-ios --no-packager",
"lint": "rnx-kit-scripts lint",
"test": "react-native rnx-test --platform ios",
"bundle": "react-native rnx-bundle",
"bundle+esbuild": "react-native rnx-bundle --id esbuild",
"bundle:android": "react-native rnx-bundle --platform android",
"bundle:ios": "react-native rnx-bundle --platform ios",
"bundle:macos": "react-native rnx-bundle --platform macos",
"bundle:windows": "react-native rnx-bundle --platform windows",
"ram-bundle": "react-native rnx-ram-bundle",
"android": "react-native run-android --no-packager --appId com.msft.identity.client.sample.local",
"ios": "react-native run-ios --no-packager",
"macos": "react-native run-macos --scheme ReactTestApp --no-packager",
"windows": "react-native run-windows --no-packager --sln windows/SampleCrossApp.sln",
"start": "react-native rnx-start"
"macos": "rnx run-macos --scheme ReactTestApp --no-packager",
"ram-bundle": "rnx ram-bundle",
"start": "rnx start",
"test": "rnx test --platform ios",
"windows": "rnx run-windows --no-packager --sln windows/SampleCrossApp.sln"
},
"dependencies": {
"@react-native-webapis/web-storage": "workspace:*",

Просмотреть файл

@ -3877,30 +3877,28 @@ __metadata:
"@rnx-kit/metro-service": "npm:^3.1.6"
"@rnx-kit/scripts": "npm:*"
"@rnx-kit/third-party-notices": "npm:^1.3.4"
"@rnx-kit/tools-filesystem": "npm:*"
"@rnx-kit/tools-language": "npm:^2.0.0"
"@rnx-kit/tools-node": "npm:^2.1.1"
"@rnx-kit/tools-react-native": "npm:^1.4.0"
"@rnx-kit/tsconfig": "npm:*"
"@types/connect": "npm:^3.4.36"
"@types/fs-extra": "npm:^9.0.0"
"@types/jest": "npm:^29.2.1"
"@types/node": "npm:^20.0.0"
"@types/node-fetch": "npm:^2.6.5"
"@types/qrcode": "npm:^1.4.2"
commander: "npm:^11.1.0"
eslint: "npm:^8.56.0"
fs-extra: "npm:^10.0.0"
jest: "npm:^29.2.1"
memfs: "npm:^4.0.0"
markdown-table: "npm:^3.0.0"
metro: "npm:^0.80.3"
metro-babel-transformer: "npm:^0.80.0"
metro-config: "npm:^0.80.3"
node-fetch: "npm:^2.6.7"
ora: "npm:^5.4.1"
prettier: "npm:^3.0.0"
qrcode: "npm:^1.5.0"
react: "npm:18.2.0"
react-native: "npm:^0.74.0"
readline: "npm:^1.3.0"
tsx: "npm:^4.15.0"
type-fest: "npm:^4.0.0"
typescript: "npm:^5.0.0"
peerDependencies:
@ -3911,6 +3909,8 @@ __metadata:
optional: true
react-native:
optional: true
bin:
rnx-cli: bin/rnx-cli.cjs
languageName: unknown
linkType: soft
@ -4555,6 +4555,9 @@ __metadata:
react-native-windows: "npm:^0.74.0"
react-test-renderer: "npm:18.2.0"
typescript: "npm:^5.0.0"
bin:
rnx: ../cli/bin/rnx-cli.cjs
rnx.reason: Workaround for Node not being able to find `rnx-cli` because of Yarn virtual packages
languageName: unknown
linkType: soft
@ -6531,6 +6534,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^11.1.0":
version: 11.1.0
resolution: "commander@npm:11.1.0"
checksum: 10c0/13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179
languageName: node
linkType: hard
"commander@npm:^2.20.0":
version: 2.20.3
resolution: "commander@npm:2.20.3"