--- url: /docs/introduction.md --- # Introduction Rock is a modular toolkit for teams building React Native apps. It helps improve build times and developer experience while fitting into your existing workflows and infrastructure. :::tip Ready to get started? Choose your path: [Getting Started →](/docs/getting-started) ::: ## Who should use Rock Rock is built for two kinds of teams: - **Existing React Native teams using Community CLI** who want to improve build times and developer experience while fitting into your existing workflows and infrastructure. - **iOS/Android teams** planning to incorporate React Native without disrupting existing workflows: Rock Brownfield lets you add your whole React Native app like any other dependency. :::info New to React Native and building app from scratch? For **new projects that aren't brownfield**, consider starting with [Expo](https://expo.dev) for the best developer experience and similar remote caching capabilities. We recommend using [this template](https://github.com/nkzw-tech/expo-app-template) for sensible defaults. Rock is designed for teams who have outgrown the Community CLI. ::: Both types of teams will benefit from Rock's cross‑platform reach: iOS, Android, and experimental HarmonyOS by default, with a flexible architecture that extends to TVs, macOS, and Windows (coming soon). ## Why We Exist At [Callstack](https://callstack.com/), we work with large teams building complex React Native apps. As maintainers of the Community CLI, we have quite the exposure to how this tool is used in various projects. These teams face similar challenges: - **Build times** – No reuse of builds across CI jobs and development teams - **Infrastructure control** – Need to host everything on their own infrastructure - **Platform diversity** – Shipping to 10+ platforms beyond iOS, Android, and HarmonyOS (experimental) - **Brownfield integration** – Embedding React Native in existing native apps - **Tech stack complexity** – Adding React Native to mixed technology environments According to the [React Native Framework RFC](https://github.com/react-native-community/discussions-and-proposals/pull/759), many companies build custom frameworks on top of Community CLI to address these needs, but most keep them internal. **Rock exists to provide a modular, production-ready solution that serves these needs out of the box.** ## Our Principles Rock is built on three core principles: - **Modular design** — add your supported platforms and plugins, and integrate existing tools; you can build around our framework - **Self-hosting** — use your own infrastructure; whether you're using GitHub Actions or Amazon S3 and BitBucket, we got you covered - **Incremental adoption** — easily migrate from Community CLI or add to an existing native app, at your own pace --- url: /docs/prior-art.md --- # Prior Art Rock wouldn’t exist without the work that came before it. We want to thank the teams who pushed forward ideas in React Native builds and developer tooling. Ideas that shaped how we think about speed, reliability, and what a truly modern build system should be. ## Expo Fingerprint and Remote Cache Expo's [`@expo/fingerprint`](https://github.com/expo/expo/tree/4991b5e35ad90ef9e022ebd2854f4bf5d88dc50d/packages/%40expo/fingerprint) introduced the concept of tying a project's native sources to a unique hash that identifies the resulting binary. Rock's remote and local build cache builds on this idea. Expo's [Fingerprint and Remote Cache implementation with GitHub Actions](https://expo.dev/blog/expo-fingerprint-github-actions) shows how this works in CI/CD pipelines. ## RNX Kit [RNX Kit](https://github.com/microsoft/rnx-kit) is Microsoft's collection of React Native tooling that includes dependency management, native builds, and better bundling. Their approach to purpose-built tools that address the complexity of React Native engineering and the fast-changing ecosystem influenced our thinking about comprehensive developer tooling. ## Expo CLI [Expo CLI](https://docs.expo.dev/more/expo-cli/) provide cloud builds and local development tools. We like their developer experience and how they make everything work together, bringing often complex packages and libraries together seamlessly. ## React Native Community CLI The [React Native Community CLI](https://github.com/react-native-community/cli) influenced Rock's design, especially since Rock's founding team maintained the CLI. Its configuration system, modular architecture, and run/build commands inform Rock's approach to extensible tooling that supports non-standard configurations and multiple platforms. --- url: /docs/getting-started.md --- import { PackageManagerTabs } from '@theme'; # Getting Started Choose your path based on your current situation: ## New React Native Project :::info Consider Expo First For **new projects**, we recommend starting with [Expo](https://expo.dev) for the best developer experience and similar remote caching capabilities. Use [this template](https://github.com/nkzw-tech/expo-app-template) for sensible defaults. Rock is designed for teams who have outgrown the Community CLI. ::: To create a new React Native project with Rock: The command will ask you to pick your preferred bundler and platforms. ## Migrate from Community CLI If you have an existing React Native project using Community CLI: This will automatically detect your existing project and guide you through the migration process. :::warning Automatic Migration Issues? If automatic migration doesn't work for your project, check the [detailed migration guide](/docs/cli/migrating-from-community-cli) for manual instructions. ::: ## Add to Existing Native Project For iOS/Android teams wanting to add React Native to existing apps, see our [Brownfield documentation](/docs/brownfield/intro) for step-by-step instructions. ## Usage Now that you have Rock configured, you should be able to use Metro's or Re.Pack's development server and bundle your application. ### Running development server When developing your application, you'll need to run a dev server that will use a bundler like Metro or Re.Pack to bundle your JS and later serve it to an app running on a device or simulator. To start the development server, run: ### Running the iOS app To build and run your app on an iOS simulator or device, run the `run:ios` command: ### Running the Android app To build and run your app on an Android emulator or device, run the `run:android` command: ### Running the HarmonyOS app (experimental) To build and run your app on a HarmonyOS emulator or device, run the `run:harmony` command: ## Next Steps - Learn about [CLI commands and features](/docs/cli/introduction) - Set up [Remote Cache](/docs/remote-cache/introduction) --- url: /docs/configuration.md --- # Configuration Rock can be configured through a configuration object that defines various aspects of your project setup. The most basic configuration would, assuming you only support iOS platform and choose Metro as our bundler, would look like this: ```js title="rock.config.mjs" // @ts-check import { platformIOS } from '@rock-js/platform-ios'; import { pluginMetro } from '@rock-js/plugin-metro'; /** @type {import('rock').Config} */ export default { bundler: pluginMetro(), platforms: { ios: platformIOS(), }, }; ``` :::info Explicit configuration It's intentional design decision to explicitly define platforms, bundlers etc, so you can e.g. add more platforms, or replace a bundler with a different one. ::: ## All Configuration Options ```typescript { // Optional: Root directory of your project root?: string; // Optional: React Native version being used reactNativeVersion?: string; // Optional: Custom path to React Native in node_modules reactNativePath?: string; // Optional: Custom bundler plugin bundler?: PluginType; // Optional: Array of plugins plugins?: Array; // Optional: Platform-specific configurations platforms?: Record; // Optional: Additional commands commands?: Array; // Optional: Configure remote cache provider. Currently supports: 'github-actions' or custom provider (function). remoteCacheProvider?: 'github-actions' | () => RemoteBuildCache | null; // Optional: Configure fingerprint options. fingerprint?: { // Additional source files/directories to include in fingerprint calculation extraSources?: string[]; // Paths to ignore when calculating fingerprints ignorePaths?: string[]; // Environmental variables that should affect fingerprints env?: string[]; }, // Optional: Whether to use prebuilt RN Core. Defaults to true. usePrebuiltRNCore?: boolean; } ``` ## Plugins A plugin is a partially applied function that has access to `api` object of `PluginApi` type: ```ts type PluginApi = { registerCommand: (command: CommandType) => void; getProjectRoot: () => string; getReactNativeVersion: () => string; getReactNativePath: () => string; getPlatforms: () => { [platform: string]: object }; getRemoteCacheProvider: () => null | undefined | (() => RemoteBuildCache); getFingerprintOptions: () => { extraSources: string[]; ignorePaths: string[]; env: string[]; }; }; ``` The following configuration options accept plugins: [`plugins`](#plugins), [`platforms`](#platforms), [`bundler`](#bundlers). A plugin that registers `my-command` command outputing a hello world would look like this: ```ts title="rock.config.mjs" const simplePlugin = (pluginConfig: SamplePluginConfig) => (api: PluginApi): PluginOutput => { api.registerCommand({ name: 'my-command', description: 'My command description', action: async (args) => { console.log('hello world'); }, }); }; export default { plugins: [simplePlugin()], }; ``` ## Bundler Bundler is a plugin that registers commands for running a dev server and bundling final JavaScript or Hermes bytecode. By default, Rock ships with two bundler: Metro (`@rock-js/plugin-metro`) and Re.Pack (`@rock-js/plugin-repack`). You can configure the bundler like this: ```js title="rock.config.mjs" import { pluginMetro } from '@rock-js/plugin-metro'; export default { // ... bundler: pluginMetro(), }; ``` ## Metro Configuration Rock uses sensible defaults from `@react-native/metro-config`, so you don't need a `metro.config.js` file for most projects. The bundler works out of the box. ### Customizing Metro If you need to customize Metro (e.g., add asset extensions, configure transformers, or set up a monorepo), create a `metro.config.js` file in your project root: ```js title="metro.config.js" const { getDefaultConfig, mergeConfig } = require('@rock-js/plugin-metro'); /** * @type {import('@rock-js/plugin-metro').MetroConfig} */ module.exports = mergeConfig(getDefaultConfig(__dirname), { // Your custom configuration }); ``` ### Common Customizations #### Adding File Extensions ```js title="metro.config.js" const { getDefaultConfig, mergeConfig } = require('@rock-js/plugin-metro'); const config = getDefaultConfig(__dirname); module.exports = mergeConfig(config, { resolver: { sourceExts: [...config.resolver.sourceExts, 'cjs', 'mjs'], assetExts: [...config.resolver.assetExts, 'ttf', 'otf'], }, }); ``` #### Monorepo Setup For monorepo projects, you'll need to configure Metro to watch additional directories: ```js title="metro.config.js" const path = require('path'); const { getDefaultConfig, mergeConfig } = require('@rock-js/plugin-metro'); const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, '../..'); const config = getDefaultConfig(projectRoot); module.exports = mergeConfig(config, { watchFolders: [workspaceRoot], resolver: { nodeModulesPaths: [ path.resolve(projectRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules'), ], }, }); ``` #### Blocking Specific Paths ```js title="metro.config.js" const { getDefaultConfig, mergeConfig } = require('@rock-js/plugin-metro'); module.exports = mergeConfig(getDefaultConfig(__dirname), { resolver: { blockList: [ /.*\/node_modules\/.*\/node_modules\/.*/, /.*\/__fixtures__\/.*/, ], }, }); ``` ### API Reference #### `getDefaultConfig(projectRoot)` Returns the default Metro configuration. Re-exported from `@react-native/metro-config`. - **projectRoot** (`string`): The root directory of your project (usually `__dirname`) - **Returns**: `MetroConfig` object #### `mergeConfig(baseConfig, overrideConfig)` Merges two Metro configurations together. Re-exported from `metro-config`. - **baseConfig** (`MetroConfig`): The base configuration - **overrideConfig** (`InputConfig`): Configuration to merge on top - **Returns**: Merged `MetroConfig` ## Platforms Platform is a plugin that registers platform-specific functionality such as commands to build the project and run it on a device or simulator. By default, Rock ships with two platforms: iOS (`@rock-js/platform-ios`) and Android (`@rock-js/platform-android`). You can configure the platform like this: ```js title="rock.config.mjs" import { platformIOS } from '@rock-js/platform-ios'; export default { // ... platforms: { // config is optional; it translates to `project` config from react-native.config.js file ios: platformIOS(config), }, }; ``` ## Remote Cache Configuration One of the key features of Rock is remote build caching to speed up your development workflow. By remote cache we mean native build artifacts (e.g. APK, or IPA binaries), which are discoverable by the user and available for download. Remote cache can live on any static storage provider, such as S3, R2, or GitHub Artifacts. For Rock to know how and where to access this cache, you'll need to define `remoteCacheProvider`, which can be either bundled with the framework (such as the one for GitHub Actions) or a custom one that you can provide. When `remoteCacheProvider` is set, the CLI will: 1. Look at local cache under `.rock/` directory for builds downloaded from a remote cache. 1. If not found, it will look for a remote build matching your local native project state (a fingerprint). 1. If not found, it will fall back to local build. Available providers you can use: - [@rock-js/provider-github](#github-actions-provider): store artifacts on GitHub Workflow Artifacts - [@rock-js/provider-s3](#aws-s3-provider): store artifacts on S3 (or Cloudflare R2) In case you would like to store native build artifacts in a different kind of remote storage, you can implement your own [custom provider](#custom-remote-cache-provider). ### Uploading artifacts to remote storage Regardless of remote cache provider set, to download native build artifats from a remote storage, you'll need to upload them first, ideally in a continuous manner. That's why the best place to put the upload logic would be your Continuous Integration server. Rock provides out-of-the-box GitHub Actions for: - [`callstackincubator/ios`](https://github.com/callstackincubator/ios): action for iOS compatible with `@rock-js/provider-github` - [`callstackincubator/android`](https://github.com/callstackincubator/android): action for Android compatible with `@rock-js/provider-github` For other CI providers you'll need to manage artifacts yourself. We recommend mimicking the GitHub Actions setup on your CI server. ### GitHub Actions provider If you store your code on GitHub, one of the easiest way to setup remote cache is through `@rock-js/provider-github` and our GitHub Actions, which will manage building, uploading and downloading your native artifacts for iOS and Android. You can configure it as follows: ```ts title="rock.config.mjs" import { providerGitHub } from '@rock-js/provider-github'; import { config } from 'dotenv'; config(); // load .env file containing GITHUB_TOKEN export default { // ...rest of the config remoteCacheProvider: providerGitHub({ owner: 'github_org', repository: 'github_repo_name', }), }; ``` GitHub provider requires a valid GitHub Personal Access Token to fetch remote cache. Typically, you'll use `.env` file to store your GitHub Personal Access Token as `GITHUB_TOKEN`, next to other project secrets securely, not exposing it to the public. ```text title=".env" GITHUB_TOKEN=token_value ``` In case you use a different env variable, you can pass it as a `token` argument to the `providerGitHub` function. #### GitHub Provider Options | Option | Type | Required | Description | | ------- | -------- | -------- | ------------------------------------------------------------------ | | `repo` | `string` | Yes | The repository name to use for the GitHub server | | `owner` | `string` | Yes | The bucket name to use for the S3 server | | `token` | `string` | No | Optional GitHub Personal Access Token to use for the GitHub server | ### AWS S3 provider If you prefer to store native build artifacts on AWS S3 or Cloudflare R2, you can use `@rock-js/provider-s3`. You can configure it as follows. ```ts title="rock.config.mjs" import { providerS3 } from '@rock-js/provider-s3'; import { config } from 'dotenv'; config(); // load .env file containing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY export default { // ...rest of the config remoteCacheProvider: providerS3({ bucket: 'your-bucket', region: 'your-region', }), }; ``` S3 provider requires a valid AWS Access Key ID and Secret Access Key to fetch remote cache. Typically, you'll use `.env` file to store your AWS Access Key ID and Secret Access Key as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, next to other project secrets securely, not exposing it to the public. ```text title=".env" AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... ``` In case you use a different env variable, you can pass it as a `accessKeyId` and `secretAccessKey` arguments to the `providerS3` function. #### S3 Provider Options | Option | Type | Required | Description | | -------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `endpoint` | `string` | No | Optional endpoint, necessary for self-hosted S3 servers or Cloudflare R2 integration | | `bucket` | `string` | Yes | The bucket name to use for the S3 server | | `region` | `string` | Yes | The region of the S3 server | | `accessKeyId` | `string` | No | The access key ID for the S3. Not required when using IAM roles or other auth methods server | | `secretAccessKey` | `string` | No | The secret access key for the S3. Not required when using IAM roles or other auth methods server | | `profile` | `string` | No | AWS profile name to use for authentication. Useful for local development. | | `roleArn` | `string` | No | Role ARN to assume for authentication. Useful for cross-account access. | | `roleSessionName` | `string` | No | Session name when assuming a role. | | `externalId` | `string` | No | External ID when assuming a role (for additional security). | | `directory` | `string` | No | The directory to store artifacts in the S3 server (defaults to `rock-artifacts`) | | `name` | `string` | No | The display name of the provider (defaults to `S3`) | | `linkExpirationTime` | `number` | No | The time in seconds for presigned URLs to expire (defaults to 24 hours) | | `publicAccess` | `boolean` | No | If true, the provider will not sign requests and will try to access the S3 bucket without authentication | | `acl` | `ObjectCannedACL` | No | ACL (Access Control List) to use for uploaded objects. Possible values: `private`, `public-read`, `public-read-write`, `authenticated-read`, `aws-exec-read`, `bucket-owner-read`, `bucket-owner-full-control`. | #### Authentication Methods The S3 provider supports multiple authentication methods through the underlying AWS SDK: - **Environment variables**: Set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optionally `AWS_SESSION_TOKEN` for temporary credentials - **IAM roles**: When running on EC2, ECS, or Lambda, the SDK automatically uses the instance/task/function role - **AWS credentials file**: Use `~/.aws/credentials` with the `profile` option - **Role assumption**: Use `roleArn` to assume a different role, optionally with `profile` as source credentials - **Temporary credentials**: Set `AWS_SESSION_TOKEN` environment variable for temporary credentials - **Public access**: Set `publicAccess: true` to explicitly disable request signing and access public S3 buckets without authentication #### Cloudflare R2 Thanks to R2 interface being compatible with S3, you can store and retrieve your native build artifacts from Cloudflare R2 storage using S3 provider. Set the `endpoint` option to point to your account storage. ```ts title="rock.config.mjs" import { providerS3 } from '@rock-js/provider-s3'; export default { // ... remoteCacheProvider: providerS3({ endpoint: 'https://${ACCOUNT_ID}.r2.cloudflarestorage.com', bucket: 'your-bucket', region: 'your-region', accessKeyId: 'access-key', secretAccessKey: 'secret-key', }), }; ``` #### Private bucket with public read access For specific scenarios, you may want to restrict upload access to CI while allowing developers to fetch artifacts without credentials. This requires setting object-level ACL on uploads and using public URLs for downloads. Configure both `acl: 'public-read'` (applied during uploads on CI) and `publicAccess: true` (enables public URL downloads when credentials are unavailable): ```ts title="rock.config.mjs" import { providerS3 } from '@rock-js/provider-s3'; const isPublicAccess = !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY; export default { // ... remoteCacheProvider: providerS3({ bucket: 'your-bucket', region: 'your-region', accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, acl: 'public-read', publicAccess: isPublicAccess, }), }; ``` ### Community providers If you're not using GitHub, or can't store artifacts on S3 bucket, you can try one of the available community providers, which often come with ready-to-use CI workflow setup. #### GitLab Provider The [`@congtuandevmobile/react-native-cache-build-gitlab`](https://github.com/congtuandevmobile/react-native-cache-build-gitlab) project provides support for GitLab CI/CD artifacts as a remote cache provider. This package allows you to use GitLab Package Registry to store and retrieve native build artifacts (iOS & Android). ### Custom remote cache provider You can plug in any remote storage by implementing the `RemoteBuildCache` interface. This section explains how to implement each method and handle the complexity that Rock manages for you. #### Interface Your provider must implement: ```ts interface RemoteBuildCache { name: string; list({ artifactName, limit, }: { artifactName: string | undefined; limit?: number; }): Promise>; download({ artifactName }: { artifactName: string }): Promise; delete({ artifactName, limit, skipLatest, }: { artifactName: string; limit?: number; skipLatest?: boolean; }): Promise>; upload({ artifactName, uploadArtifactName, }: { artifactName: string; uploadArtifactName?: string; // e.g. 'ad-hoc//' }): Promise<{ name: string; url: string; id?: string; getResponse: ( buffer: Buffer | ((baseUrl: string) => Buffer), contentType?: string, ) => Response; }>; } ``` #### list Return a list of artifacts with at least `name` and a downloadable `url`. Optionally add an `id`. :::info The artifacts are uploaded as ZIP archives (excluding ad-hoc scenario), so make sure to append the `.zip` suffix to the `artifactName`. ::: **Example (S3-style):** prefix-filter objects and convert each to `{ name, url }`. Signed URLs are fine. ```ts async list({ artifactName, limit }) { const artifacts = await this.s3.send( new ListObjectsV2Command({ Bucket: this.bucket, Prefix: artifactName ? `${this.directory}/${artifactName}.zip` : `${this.directory}/`, }) ); const results = []; for (const artifact of artifacts.Contents ?? []) { if (!artifact.Key) continue; const name = artifactName ?? artifact.Key.split('/').pop() ?? ''; const presignedUrl = await getSignedUrl(/* ... */); results.push({ name, url: presignedUrl }); } return results; } ``` #### download Return a Web `Response` whose `body` is a readable stream of the artifact and (if available) a `content-length` header. Rock uses this to report download progress. :::info The artifacts are uploaded as ZIP archives (excluding ad-hoc scenario), so make sure to append the `.zip` suffix to the `artifactName`. ::: If your SDK returns a Node stream, convert it to a Web stream and wrap in `Response`: ```ts function toWebStream(node: Readable): ReadableStream { return new ReadableStream({ start(controller) { node.on('data', (chunk) => controller.enqueue(chunk)); node.on('end', () => controller.close()); node.on('error', (e) => controller.error(e)); }, }); } async download({ artifactName }) { const res = await this.s3.send( new GetObjectCommand({ Bucket: this.bucket, Key: `${this.directory}/${artifactName}.zip`, }) ); return new Response(toWebStream(res.Body), { headers: { 'content-length': String(res.ContentLength ?? ''), }, }); } ``` #### delete Delete the requested artifact(s) and return the list of deleted entries: `{ name, url, id? }`. :::info The artifacts are uploaded as ZIP archives (excluding ad-hoc scenario), so make sure to append the `.zip` suffix to the `artifactName`. ::: Respect `skipLatest` if your backend supports ordering/versioning, as it's used to clean up stale artifacts e.g. created in an open pull request. Otherwise you may simply delete the single matching object. ```ts async delete({ artifactName, skipLatest }) { if (skipLatest) { // Skip the latest artifact - implement based on your backend's versioning return []; } await this.s3.send( new DeleteObjectCommand({ Bucket: this.bucket, Key: `${this.directory}/${artifactName}.zip`, }) ); return [{ name: artifactName, url: `${this.bucket}/${this.directory}/${artifactName}.zip`, }]; } ``` #### upload Rock expects `upload()` to return metadata and a `getResponse` function: - `getResponse(buffer, contentType?) => Response`: - Rock calls this to initiate the upload and to surface upload progress - It passes either: - a `Buffer` (for normal builds), or - a function `(baseUrl) => Buffer` (for ad‑hoc pages) so you can inject absolute URLs into HTML/plist before upload - You should start the actual upload here and return a `Response` object - Rock will read that stream to display progress - for ad-hoc scenario `upload` will pass the `uploadArtifactName` variable, so use that instead of `artifactName` **For progress signaling, you can:** - Stream the original buffer in chunks, or - Use your SDK's progress events (e.g. S3's `httpUploadProgress`) to enqueue chunks proportional to actual bytes uploaded **Example (S3-like) using real SDK progress:** ```ts async upload({ artifactName, uploadArtifactName }) { const key = uploadArtifactName ? `${this.directory}/${uploadArtifactName}` : `${this.directory}/${artifactName}.zip`; const presignedUrl = await getSignedUrl(/* ... */); return { name: artifactName, url: presignedUrl, getResponse: (buffer, contentType) => { const upload = new Upload({ client: this.s3, params: { Bucket: this.bucket, Key: key, Body: buffer, ContentType: contentType ?? 'application/octet-stream', Metadata: { createdAt: new Date().toISOString() }, }, }); const stream = new ReadableStream({ start(controller) { let last = 0; upload.on('httpUploadProgress', ({ loaded, total }) => { if (loaded != null && total != null && loaded > last) { controller.enqueue(buffer.subarray(last, loaded)); last = loaded; if (loaded >= total) controller.close(); } }); upload.done().catch((e) => controller.error(e)); }, }); return new Response(stream, { headers: { 'content-length': String(buffer.length), 'content-type': contentType ?? 'application/octet-stream', }, }); }, }; } ``` #### What ends up on the provider - **Normal builds:** Rock uploads a single build artifact (a ZIP archive). Your provider stores it at a path like `/.zip`. - For iOS simulator builds (APP directory), Rock creates a temporary `app.tar.gz` to preserve permissions and includes it in the artifact; you just receive the buffer via `getResponse`. You don't need to create the tarball yourself. - **Ad-hoc distribution:** - with `--ad-hoc` flag passed to `remote-cache upload` Rock uploads: - **iOS**: The signed IPA at `/ad-hoc//.ipa`, an `index.html` landing page, and a `manifest.plist` file - **Android**: The signed APK at `/ad-hoc//.apk` and an `index.html` landing page This `index.html` file will display an ad-hoc distribution web portal, allowing developers and testers to install apps on their devices by simply clicking "Install App" (iOS) or "Download APK" (Android). Learn more about ad-hoc distribution and how it works with `remote-cache upload --ad-hoc` command [here](./cli/introduction#ad-hoc-distribution). | Ad-hoc distribution web portal | Ad-hoc distribution web portal | | ------------------------------------ | ------------------------------------- | | ![](./assets/ad-hoc-portal-dark.png) | ![](./assets/ad-hoc-portal-light.png) | #### Notes and tips - If your backend cannot support uploads, throw in `upload()` with a link to docs (as GitHub provider does). - Always return valid, downloadable `url`s from `list()`; signed URLs are OK. - Prefer setting `content-length` on both download and upload `Response` objects so Rock can display progress. - For uploads, it's fine to start the SDK upload in the background; Rock drains the returned `Response` to show progress, and your SDK promise resolves independently. In tests, mock your SDK's upload to resolve quickly. **Example provider:** ```ts import type { RemoteBuildCache } from '@rock-js/tools'; class DummyLocalCacheProvider implements RemoteBuildCache { name = 'dummy'; async list({ artifactName }) { const url = new URL(`${artifactName}.zip`, import.meta.url); return [{ name: artifactName, url }]; } async download({ artifactName }) { const artifacts = await this.list({ artifactName }); const filePath = artifacts[0].url.pathname; const fileStream = fs.createReadStream(filePath); return new Response(fileStream); } async delete({ artifactName }) { // optional... } async upload({ artifactName, uploadArtifactName }) { // optional... } } const pluginDummyLocalCacheProvider = (options) => () => new DummyLocalCacheProvider(options); ``` Then use it in your config: ```ts title="rock.config.mjs" export default { // ... remoteCacheProvider: pluginDummyLocalCacheProvider(options), }; ``` ### Opt-out of remote cache If you only want to use the CLI without the remote cache, and skip the steps `1.` and `2.` and a warning that you're not using a remote provider, you can disable this functionality by setting it to `null`: ```ts export default { // ... remoteCacheProvider: null, }; ``` ## Fingerprint Configuration A fingerprint is a representation of your native project in a form of a hash (e.g. `378083de0c6e6bb6caf8fb72df658b0b26fb29ef`). It's calculated every time the CLI is run. When a local fingerprint matches the one that's generated on a remote server, we have a match and can download the project for you instead of building it locally. The fingerprint configuration helps determine when builds should be cached and invalidated in non-standard settings: - `extraSources`: when you have git submodules in your project - `ignorePaths`: custom directories that are not relevant for the native build state - `env`: names of environment variables that should affect the fingerprint When you configure `env`, pass the environment variable names, not their resolved values. Rock reads each name from `process.env` when calculating the fingerprint. ```ts export default { // ... fingerprint: { extraSources: ['./git-submodule'], ignorePaths: ['./temp'], env: ['CUSTOM_ENV'], }, }; ``` --- url: /docs/cli/introduction.md --- # Introduction The Rock CLI is a command-line tool that helps you develop, build, and run React Native applications. We've created a new CLI from scratch with a focus on seamless migration from the Community CLI. Most projects can get started with our CLI in under 10 minutes. At its core is a modular configuration system that lets you customize capabilities through plugins and replaceable build chain components: bundlers, platforms, remote cache providers, and other helpers available as npm packages. Basic usage: ```shell title="Terminal" npx rock [command] [options] ``` ![](/cli.png) ## Key Features The CLI handles all essential build and deployment tasks: - Building and running APK/APP/HAP files on devices and simulators - Creating builds for different variants and configurations - Generating signed IPA and AAB archives for app stores - Re-signing archives with fresh JS bundles - Generating native project hashes for caching ## Command Changes from Community CLI We've updated command names: - `run-android` → `run:android` - `build-android` → `build:android` - `run-ios` → `run:ios` - `build-ios` → `build:ios` ## Flag Changes We've standardized flag naming across platforms: Android: - `--mode` → `--variant` - `--appId` → `--app-id` - `--appIdSuffix` → `--app-id-suffix` iOS: - `--mode` → `--configuration` - `--buildFolder` → `--build-folder` ## Removed Flags We've simplified the interface by removing redundant flags: - `--interactive`/`-i` – CLI now prompts for input when needed - `--list-devices` – Device selection is now automatic when no devices are connected ## Remote Cache The CLI integrates with Rock's Remote Cache system to speed up builds by reusing cached native artifacts. When available, the CLI will automatically download and use cached builds (APK/AAB/APP/IPA) instead of rebuilding from scratch. Learn more about [Remote Cache & GitHub Actions](/docs/remote-cache/introduction). ## Local Cache Regardless of the remote cache provider you use, the CLI will also cache builds (APK/AAB/APP/IPA) in your local cache (`.rock/` directory). If a cached build is found, it will be used instead of rebuilding from scratch. ## Global Options The following options are available for all commands: | Options | Description | | ------------------- | ------------------------------- | | `-h` or `--help` | Shows all available options | | `-V` or `--version` | Outputs the Rock version number | | `--verbose` | Sets verbose logging | ## Available Commands Rock CLI uses a modular design where available commands depend on your configuration. The following commands are available by default for all configurations (these are internal commands that you typically won't need to run): | Command | Description | | :------------- | :---------------------------------------------- | | `config` | Outputs autolinking config (from Community CLI) | | `fingerprint` | Calculates fingerprint for project or platform | | `clean` | Cleans various caches to free up disk space | | `help` | Displays help menu for a command | | `remote-cache` | Manage remote cache | Additional commands for development, building, and running apps are provided by specialized plugins. ### Bundler Plugins Bundler plugins are configured through the [`bundler`](/docs/configuration/index#bundler) property in your configuration. Available bundlers include: - `@rock-js/plugin-metro` – Metro bundler plugin with the following commands: | Command | Description | | :------- | :---------------------------- | | `start` | Starts Metro dev server | | `bundle` | Bundles JavaScript with Metro | - `@rock-js/plugin-repack` – Re.Pack bundler plugin with the following commands: | Command | Description | | :------- | :------------------------------ | | `start` | Starts Re.Pack dev server | | `bundle` | Bundles JavaScript with Re.Pack | ### Platform Plugins Platform plugins are configured through the [`platform`](/docs/configuration/index#platforms) property in your configuration. Available platforms include: - `@rock-js/platform-android` – Android platform plugin with the following commands: | Command | Description | | :------------------------- | :-------------------------------------------------------------- | | `run:android` | Runs Android app on emulator or device | | `build:android` | Builds Android app for generic emulator, device or distribution | | `sign:android` | Signs Android app with keystore | | `validate-elf-alignment` | Validates ELF alignment of shared libraries in an APK | - `@rock-js/platform-ios` – iOS platform plugin with the following commands: | Command | Description | | :---------- | :----------------------------------------------------------- | | `build:ios` | Builds iOS app for generic simulator, device or distribution | | `run:ios` | Runs iOS app on simulator or device | | `sign:ios` | Signs iOS app with certificate and provisioning profile | - `@rock-js/platform-harmony` – HarmonyOS platform plugin (experimental) with the following commands: | Command | Description | | :-------------- | :------------------------------------------ | | `build:harmony` | Builds HarmonyOS app for emulator or device | | `run:harmony` | Runs HarmonyOS app on device | ## Platform iOS ### `rock build:ios` Options The `build:ios` command builds your iOS app for simulators, devices, or distribution, producing either an APP directory (for simulators) or an IPA file (for devices and distribution). | Option | Description | | :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--configuration ` | Xcode scheme configuration (case sensitive) | | `--scheme ` | Xcode scheme to use | | `--target ` | Xcode target to use | | `--extra-params ` | Custom xcodebuild parameters | | `--export-extra-params ` | Custom xcodebuild export archive parameters | | `--export-options-plist ` | Export options file for archiving (default: ExportOptions.plist) | | `--build-folder ` | Location for iOS build artifacts | | `--destination ` | Define destination(s) for the build. You can pass multiple destinations as separate values or repeated use of the flag. Values can be either: "simulator", "device" or destinations supported by "xcodebuild -destination" flag, e.g. "generic/platform=iOS" | | `--archive` | Create Xcode archive (IPA) | | `--no-install-pods` | Skip CocoaPods installation | | `--no-new-arch` | Build in legacy async architecture | #### Supported environmental variables The `build:ios` command supports the following environmental variables that are passed to `pod` command that installs CocoaPods dependencies: | Variable | Description | Default | | ------------------------- | -------------------------------------------------------------------------------------------- | ------- | | `RCT_USE_RN_DEP` | Use prebuilt React Native dependencies for faster compilation (only for React Native v0.81+) | `1` | | `RCT_USE_PREBUILT_RNCORE` | Use prebuilt React Native core for faster compilation (only for React Native v0.81+) | `1` | | `USE_THIRD_PARTY_JSC` | Use JavaScriptCore instead of Hermes for JavaScript execution | `0` | To change these variables, you can prefix the `build:ios` command with environmental variables. For example, to use prebuilt React Native dependencies and core for faster compilation, you can use the following command: ```shell RCT_USE_PREBUILT_RNCORE=0 RCT_USE_RN_DEP=0 npx rock build:ios ``` ### `rock run:ios` Options The `run:ios` command runs your iOS app on a simulator or device. It follows this build strategy: 1. Use the provided binary if specified with `--binary-path` 1. Build locally if `--local` flag is set 1. Otherwise, try to use a cached build from cache (in `.rock` folder) The build cache is populated either by a local build or when downloaded frome remote storage with [`remoteCacheProvider`](../configuration.md#remote-cache-configuration). `run:ios` extends the functionality of `build:ios` with additional runtime options. | Option | Description | | :----------------------- | :------------------------------------------------------------------------- | | `--port ` | Bundler port (default: 8081) | | `--binary-path ` | Path to pre-built .app binary | | `--device ` | Device/simulator to use (by name or UDID) | | `--catalyst` | Run on Mac Catalyst | | `--local` | Force local build with xcodebuild | | `--dev-server` | Automatically start a dev server (bundler) after building the app. | | `--host` | Specify a custom host for the dev server (bundler) after building the app. | You can also pass the same environmental variables listed in [`build:ios` options](#supported-environmental-variables) to the `run:ios` command. ### `rock sign:ios` Options The `sign:ios` command either signs your iOS app with certificates and provisioning profiles, producing a signed IPA file ready for distribution, or modifies APP file without signing. It allows for replacing the JS bundle with a new version. | Argument | Description | | :----------- | :-------------------------- | | `binaryPath` | Path to the IPA or APP file | | Option | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------ | | `--app` | Modify APP file (directory) instead of IPA file. No signing is done | | `--identity ` | Certificate Identity name for code signing | | `--output ` | Path to output IPA file | | `--build-jsbundle` | Build JS bundle before signing | | `--jsbundle ` | Path to JS bundle to apply before signing | | `--no-hermes` | Don't use Hermes for JS bundle | | `--use-app-entitlements` | Extract app bundle codesigning entitlements and combine with entitlements from new provisioning profile | ## Platform Android ### `rock build:android` Options The `build:android` command builds your Android app for emulators, devices, or distribution, producing either APK or AAB files. It follows this build strategy: 1. Use the provided binary if specified with `--binary-path` 1. Build locally if `--local` flag is set 1. Otherwise, try to use a cached build from cache (in `.rock` folder) The build cache is populated either by a local build or when downloaded frome remote storage with [`remoteCacheProvider`](../configuration.md#remote-cache-configuration). | Option | Description | | :----------------------- | :-------------------------------------- | | `--variant ` | Build variant (debug/release) | | `--aab` | Build Android App Bundle instead of APK | | `--active-arch-only` | Build only for active architecture | | `--tasks ` | Custom Gradle tasks | | `--extra-params ` | Extra parameters for Gradle | ### `rock run:android` Options The `run:android` command runs your Android app on an emulator or device. It extends the functionality of `build:android` with additional runtime options. Same as for `build:android` and: | Option | Description | | :------------------------- | :------------------------------------------------------------------------- | | `--app-id ` | Application ID | | `--app-id-suffix ` | Application ID suffix | | `--binary-path ` | Path to pre-built APK | | `--local` | Force local build with Gradle wrapper | | `--dev-server` | Automatically start a dev server (bundler) after building the app. | | `--host` | Specify a custom host for the dev server (bundler) after building the app. | ### `rock sign:android` Options The `sign:android ` command signs your Android app with a keystore, producing a signed APK or AAB file ready for distribution. It allows for replacing the JS bundle with a new version. | Argument | Description | | :----------- | :-------------------------- | | `binaryPath` | Path to the APK or AAB file | | Option | Description | | :----------------------------- | :---------------------------------------- | | `--keystore ` | Path to keystore file | | `--keystore-password ` | Password for keystore file | | `--output ` | Path to output APK or AAB file | | `--build-jsbundle` | Build JS bundle before signing | | `--jsbundle ` | Path to JS bundle to apply before signing | | `--no-hermes` | Don't use Hermes for JS bundle | ### `rock validate-elf-alignment` Options The `validate-elf-alignment` command validates that shared libraries (`.so` files) inside an APK are properly aligned to 16KB page boundaries. Starting with Android 15, the Google Play Store requires 64-bit shared libraries (`arm64-v8a`, `x86_64`) to be aligned to 16KB pages. See [Android documentation on 16KB page sizes](https://developer.android.com/guide/practices/page-sizes#build-app-16kb) for more details. This command is based on the [check_elf_alignment.sh](https://cs.android.com/android/platform/superproject/main/+/main:system/extras/tools/check_elf_alignment.sh) script from the Android platform source. The command performs two checks: 1. **Zip alignment check** (optional) — runs `zipalign` to verify APK-level alignment. Requires Android Build-Tools 35.0.0-rc3 or higher. 2. **ELF alignment check** — extracts shared libraries from the APK and inspects each ELF binary's `LOAD` segment alignment using `objdump`. Alignment of `2**14` (16KB) or higher is considered valid. All unaligned libraries are listed, and the critical 64-bit ones (`arm64-v8a`/`x86_64`) are highlighted separately. Only unaligned 64-bit libraries cause the check to fail. | Argument | Description | | :----------- | :------------------- | | `binaryPath` | Path to the APK file | ## Platform HarmonyOS (experimental) :::warning HarmonyOS integration is currently experimental and not fully feature complete with iOS and Android platforms. The API and functionality may change in future releases. Missing functionality: - Ready to use GitHub Action - Re-signing with `sign:harmony` command - Running on emulator (DevEco Studio doesn't allow for emulators outside of China) ::: ### `rock build:harmony` Options The `build:harmony` command builds your HarmonyOS app for emulators or devices, producing HAP files. It follows this build strategy: 1. Build locally if `--local` flag is set 1. Otherwise, try to use a cached build from cache (in `.rock` folder) The build cache is populated by a local build only for now (remote cache is not supported yet). | Option | Description | | :---------------------- | :---------------------------- | | `--build-mode ` | Build mode (debug/release) | | `--module ` | Module to build | | `--product ` | Product to build | | `--local` | Force local build with Hvigor | ### `rock run:harmony` Options The `run:harmony` command runs your HarmonyOS app on an emulator or device. It extends the functionality of `build:harmony` with additional runtime options. Same as for `build:harmony` and: | Option | Description | | :----------------------- | :------------------------------------- | | `--port ` | Bundler port (default: 8081) | | `--build-mode ` | Build mode (debug/release) | | `--product ` | Product to build | | `--binary-path ` | Path to pre-built HAP binary | | `--device ` | Device/emulator to use (by name or ID) | | `--local` | Force local build with Hvigor | | `--ability ` | Name of the ability to start | ## Plugin Bundler ### `rock start` Options The `start` command launches a development server (either Re.Pack or Metro, depending on your bundler plugin) that connects to your apps through port 8081 by default. It provides features like Hot Module Reloading (HMR) and error reporting. | Option | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------ | | `--port ` | Port to run the server on (default: 8081) | | `--host ` | Host to run the server on (default: "") | | `--project-root `, `--projectRoot ` | Path to a custom project root | | `--watch-folders `, `--watchFolders ` | Specify any additional folders to be added to the watch list | | `--asset-plugins `, `--assetPlugins ` | Specify any additional asset plugins to be used by the packager by full filepath | | `--source-exts `,`--sourceExts ` | Specify any additional source extensions to be used by the packager | | `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | | `--transformer ` | Specify a custom transformer to be used | | `--reset-cache`, `--resetCache` | Removes cached files | | `--custom-log-reporter-path ` | Path to a JavaScript file that exports a log reporter as a replacement for TerminalReporter | | `--https` | Enables https connections to the server | | `--key ` | Path to custom SSL key | | `--cert ` | Path to custom SSL cert | | `--config ` | Path to the CLI configuration file | | `--no-interactive` | Disables interactive mode | | `--client-logs` | [Deprecated] Enable plain text JavaScript log streaming for all connected apps | ### `rock bundle` Options The `bundle` command creates an optimized JavaScript bundle for your application, optionally using Hermes bytecode. | Option | Description | | :-------------------------------------- | :--------------------------------------------------------------------------------------------------- | | `--entry-file ` | Path to the root JS file, either absolute or relative to JS root | | `--platform ` | Either "ios", "android", or "harmony" (default: "ios") | | `--transformer ` | Specify a custom transformer to be used | | `--dev [boolean]` | If false, warnings are disabled and the bundle is minified (default: true) | | `--minify [boolean]` | Allows overriding whether bundle is minified. Defaults to false if dev is true, true if dev is false | | `--bundle-output ` | File name where to store the resulting bundle, ex. /tmp/groups.bundle | | `--bundle-encoding ` | Encoding the bundle should be written in (default: "utf8") | | `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | | `--sourcemap-output ` | File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map | | `--sourcemap-sources-root ` | Path to make sourcemap's sources entries relative to, ex. /root/dir | | `--sourcemap-use-absolute-path` | Report SourceMapURL using its full path (default: false) | | `--assets-dest ` | Directory name where to store assets referenced in the bundle | | `--unstable-transform-profile ` | Experimental, transform JS for a specific JS engine (default: "default") | | `--asset-catalog-dest [string]` | Path where to create an iOS Asset Catalog for images | | `--reset-cache` | Removes cached files (default: false) | | `--read-global-cache` | Try to fetch transformed JS code from the global cache, if configured (default: false) | | `--config ` | Path to the CLI configuration file | | `--resolver-option ` | Custom resolver options of the form key=value. URL-encoded. May be specified multiple times | | `--config-cmd [string]` | [Internal] A hack for Xcode build script pointing to wrong bundle command | | `--hermes` | Passes the output JS bundle to Hermes compiler and outputs a bytecode file | ## Built-in plugins ### `rock fingerprint` Options The `fingerprint` command calculates a unique hash that represents your project's native state. This hash is used for build caching and remains stable across builds unless you modify native files, change dependencies with native code, or update scripts in package.json. | Option | Description | | :------------------------ | :--------------------------------------------- | | `-p, --platform ` | Select platform, e.g. ios, android, or harmony | | `--raw` | Output the raw fingerprint hash for piping | **Arguments:** - `[path]` - Directory to calculate fingerprint for (optional) ### `rock config` Options The `config` command outputs the autolinking configuration from Community CLI, which is useful for debugging and understanding how dependencies are linked. | Option | Description | | :------------------------ | :--------------------------------------------- | | `-p, --platform ` | Select platform, e.g. ios, android, or harmony | ### `rock clean` Options The `clean` command helps you free up disk space by removing various caches and temporary files from your React Native project. It can clean Android (Gradle), iOS (CocoaPods), Metro, Watchman, Rock's own project caches, package manager caches, and CCache compiler cache. | Option | Description | | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--include ` | Comma-separated list of caches to clean. Available options: `android`, `gradle`, `cocoapods`, `metro`, `watchman`, `npm`, `yarn`, `bun`, `pnpm`, `rock`, `ccache` | | `--verify-cache` | Whether to verify the cache (currently only applies to npm cache) | | `--all` | Clean all available caches without interactive prompt | ## Plugin Remote Cache ### `rock remote-cache` Actions and Options The `remote-cache ` command provides utilities to interact with the remote build cache configured via your `remoteCacheProvider`. This is useful for inspecting, downloading, uploading, or deleting build artifacts stored remotely. Available actions: | Action | Description | | :------------------ | :-------------------------------------------------------------------------------- | | `list` | Lists the latest artifact matching the specified criteria | | `status` | Reports whether the current fingerprint has a matching cached artifact | | `list-all` | Lists all artifacts (optionally filtered by platform and traits) | | `download` | Downloads an artifact from remote cache to local cache | | `upload` | Uploads a binary to remote cache. Accepts `--ad-hoc` flag for Ad-Hoc distribution | | `delete` | Deletes artifacts from remote cache | | `get-provider-name` | Returns the name of the configured remote cache provider | Actions have different options available: | Option | Description | | :------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--json` | Output results in JSON format instead of human-readable format | | `--name ` | Full artifact name to operate on. Cannot be used with `--platform` or `--traits` | | `--all` | List or delete all matching artifacts (affects `list` and `delete` actions only) | | `--all-but-latest` | Delete all but the latest matching artifact (affects `delete` action only) | | `-p, --platform ` | Platform to target (`ios`, `android`, or `harmony`). Must be used with `--traits` | | `-t, --traits ` | Comma-separated traits that construct the final artifact name. For Android: variant (e.g., `debug`, `release`). For iOS: destination and configuration (e.g., `simulator,Release`) | | `--binary-path ` | Path to the binary to upload (used with `upload` action) | | `--ad-hoc ` | Upload binary for ad-hoc distribution and installation from URL. **iOS**: Uploads IPA, index.html, and manifest.plist. **Android**: Uploads APK and index.html | For example, to download remote cache for iOS simulator with Release configuration, you can use `remote-cache download` with `--name` option ```shell npx rock remote-cache download --name rock-ios-simulator-Release-abc123fbd28298 ``` or pass `--traits`, so you don't need to pass the fingerprint: ```shell npx rock remote-cache download --platform ios --traits simulator,Release ``` To check whether the current fingerprint has a matching cached artifact without downloading it: ```shell npx rock remote-cache status --platform ios --traits device,AdHoc --json ``` The JSON output includes the provider, fingerprint, artifact name, hit status, and matching artifact information when one exists. It supports both bundled remote cache providers: GitHub Actions and S3 (including S3-compatible storage). The command fails when the provider lookup fails. #### Ad-hoc distribution Ad-hoc distribution allows you to share your mobile app with testers without going through the App Store or Play Store. Testers can install your app directly on their devices by visiting a web page. **What is Ad-hoc distribution?** Ad-hoc distribution is a method for sharing mobile apps with testers without going through official app stores. It's perfect for beta testing, internal testing, or client demos. For iOS, devices must be registered in your Apple Developer account. For Android, testers need to enable "Install from Unknown Sources" in their device settings. Apps installed this way will appear on the device's home screen just like any other app. **How it works:** 1. Build your app with proper configuration ```shell # iOS - requires valid provisioning profile that includes your testers devices npx rock build:ios --archive # ...other required flags # Android - requires signing with keystore (use any signed variant e.g., release) npx rock build:android --variant release # ...other required flags ``` 2. Use `upload --ad-hoc` to upload the app for ad-hoc distribution ```shell # iOS npx rock remote-cache upload --ad-hoc --platform ios --traits device,Release # Android - traits should match your build variant npx rock remote-cache upload --ad-hoc --platform android --traits release ``` 3. Share the generated URL with your testers 4. Testers visit the URL and install the app on their device (iOS: tap "Install App"; Android: Download APK and install) The command creates a special folder structure that includes: - Your signed binary (IPA for iOS, APK for Android) - An HTML page for easy installation (you need to configure your provider to **make this file publicly available**) - A manifest.plist file (iOS only) The folder will be available at `ad-hoc/` directory of your configured remote cache provider. --- url: /docs/cli/migrating-from-community-cli.md --- import { PackageManagerTabs } from '@theme'; # Migrating from Community CLI ## Automatic Migration You can automate all the migration steps below by running the following command in your existing React Native project. The CLI will detect your project and guide you through the migration process interactively, automatically updating all necessary files and configurations. ## Manual Migration If you prefer to do it manually or encounter any issues, follow the steps below. 1. Install dev dependencies: 1. Remove `@react-native-community/cli` and related packages. 1. Add `.rock/` folder with caches to `.gitignore`: ```txt title=".gitignore" .rock/ ``` 1. Add `rock.config.mjs` file: ```js title="rock.config.mjs" // @ts-check import { platformIOS } from '@rock-js/platform-ios'; import { platformAndroid } from '@rock-js/platform-android'; import { pluginMetro } from '@rock-js/plugin-metro'; /** @type {import('rock').Config} */ export default { bundler: pluginMetro(), platforms: { ios: platformIOS(), android: platformAndroid(), }, remoteCacheProvider: 'github-actions', }; ``` Move any `project` config from `react-native.config.js` to platform arguments in `rock.config.mjs`, for example: ```js title="react-native.config.js module.exports = { project: { ios: { sourceDir: 'custom-source', }, android: { appName: 'custom', }, }, }; ``` translates to: ```js title="rock.config.mjs" export default { platforms: { ios: platformIOS({ sourceDir: 'custom-source' }), android: platformAndroid({ appName: 'custom' }), }, }; ``` 1. Update Android files: In `android/app/build.gradle` set the `cliFile` with the new path: ```groovy title="android/app/build.gradle" {2} // cliFile = file("../../node_modules/react-native/cli.js") cliFile = file("../../node_modules/rock/dist/src/bin.js") ``` In `android/settings.gradle` change: ```groovy title="android/settings.gradle" {2} // extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['npx', 'rock', 'config', '-p', 'android']) } ``` 1. Update iOS files: In `ios/Podfile` change: ```ruby title="ios/Podfile" {2} # config = use_native_modules! config = use_native_modules!(['npx', 'rock', 'config', '-p', 'ios']) ``` In "Bundle React Native code and images" Build Phase in Xcode add: ```shell title="Bundle React Native code and images build phase" {2-9} set -e if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then source "$PODS_ROOT/../.xcode.env" fi if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then source "$PODS_ROOT/../.xcode.env.local" fi export CONFIG_CMD="dummy-workaround-value" export CLI_PATH="$("$NODE_BINARY" --print "require('path').dirname(require.resolve('rock/package.json')) + '/dist/src/bin.js'")" WITH_ENVIRONMENT="$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh" ``` 1. Cleanup native files: ```sh git clean -fdx ios/ android/ ``` 1. Update scripts in `package.json`: ```json title="package.json" { "scripts": { "start": "rock start", "android": "rock run:android", "ios": "rock run:ios" } } ``` 1. Run new commands: ```sh npx rock run:android npx rock run:ios ``` Additionally rename flags: - `--mode` to `--variant` for Android commands - `--mode` to `--configuration` for iOS commands - `--buildFolder` to `--build-folder` for iOS commands - `--appId` to `--app-id` for Android commands - `--appIdSuffix` to `--app-id-suffix` for Android commands And remove unsupported flags: - `--interactive`/`-i` – the CLI will prompt you for input where necessary - `--list-devices` - when no devices are connected, you'll be prompt with a full device selection 1. Configure GitHub Actions for remote builds in your workflow iOS: ```yaml title=".github/workflows/build-ios" - name: Rock Remote Build - iOS simulator id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: simulator github-token: ${{ secrets.GITHUB_TOKEN }} configuration: Debug ``` Android: ```yaml title=".github/workflows/build-android" - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: debug github-token: ${{ secrets.GITHUB_TOKEN }} ``` For more setup options see [GitHub Actions configuration](../remote-cache/github-actions-setup.md) --- url: /docs/remote-cache/introduction.md --- # Remote Cache Remote Cache is a feature that speeds up your development workflow by centralizing storage for native app builds. These builds can be retrieved either manually or through our CLI, dramatically reducing build times across your team. ## What is Remote Cache? The Remote Cache acts as a centralized storage for native app builds that can be hosted on various platforms: - GitHub Actions - Amazon S3 - Cloudflare R2 - Custom providers ## Ready-to-Use Actions Rock ships with ready-to-use GitHub Actions: - [`callstackincubator/ios`](https://github.com/callstackincubator/ios) - iOS builds and caching - [`callstackincubator/android`](https://github.com/callstackincubator/android) - Android builds and caching These actions automatically store native artifacts that can be reused across CI jobs and your local development environment through the Rock CLI. ## How It Works 1. For each build, we calculate a unique hash (fingerprint) that represents your project's native state 2. This hash remains stable across builds unless you: - Modify native files - Change dependencies with native code - Update scripts in package.json 3. When you make JavaScript-only changes, the hash stays the same 4. The CLI checks for matching builds in: - Local cache (`.rock/` directory) - Remote storage - Falls back to local build if no match is found ![How CLI works with remote cache](/cli-remote-cache.png) --- url: /docs/remote-cache/github-actions-setup.md --- # Configuration Rock ships with a ready-to-use GitHub Actions: - [`callstackincubator/ios`](https://github.com/callstackincubator/ios) - [`callstackincubator/android`](https://github.com/callstackincubator/android) which you can include in your GHA workflows to build iOS and Android apps and store native artifacts to reuse across CI jobs and local dev environment through Rock CLI. ## GitHub Workflow Setup This is the recommended base setup for a GitHub Workflow file running our GitHub Actions that: - Runs the workflow on pushes to the `main` branch - Runs the workflow on pull requests to any branch ```yaml on: push: branches: - main pull_request: branches: - '**' concurrency: group: remote-build-ios-${{ github.ref }} ``` ## Setup GitHub Personal Access Token You'll be asked about this token when cached build is available while running the `rock run:ios` or `rock run:android` commands. The token is necessary for downloading cached builds. ### Fine-grained tokens for organizations Generate a [fine-grained Personal Access Token](https://github.com/settings/personal-access-tokens/new) and set **Resource owner** to your organization. Ensure the following repository permissions: - Actions: Read - Contents: Read - Metadata: Read-only ![Fine-grained Personal Access Token](../assets/github-pat.png) ### Classic tokens for individual developers Generate [GitHub Personal Access Token](https://github.com/settings/tokens/new?scopes=repo) for downloading cached builds with `repo` permissions. ### Using GitHub PAT securely in `rock.config.mjs` Typically, you'll use `.env` file to store your GitHub Personal Access Token, next to other project secrets securely, not exposing it to the public. Here, we'll use the `dotenv` package to load the `.env` file: ```ts title="rock.config.mjs" import { providerGitHub } from '@rock-js/provider-github'; import { config } from 'dotenv'; config(); // load .env file containing GITHUB_TOKEN export default { // ...rest of the config remoteCacheProvider: providerGitHub({ owner: 'github_org', repository: 'github_repo_name', }), }; ``` ## Optimizing CI/CD Performance with paths-ignore When using GitHub Actions workflows with Rock, you can optimize your CI/CD pipelines by using `paths-ignore` to skip unnecessary workflow runs. This can significantly reduce CI time and costs, especially in large repositories where not all changes require rebuilding the mobile applications. ### How to implement paths-ignore Add a `paths-ignore` section to your workflow's trigger configuration to specify which file patterns should not trigger the workflow: ```yaml name: Mobile Build on: push: branches: - main paths-ignore: - '*.md' # Skip documentation changes - 'docs/**' # Skip documentation directory - '.github/ISSUE_TEMPLATE/**' # Skip issue templates - 'web/**' # Skip web-specific code - 'server/**' # Skip backend code - 'design/**' # Skip design files # You can set similar config for pull_request hook ``` ## Next steps With this base setup, you are now ready to follow the [iOS](./ios.md) and [Android](./android.md) instructions that will get you through setting up the GitHub Actions `jobs` for building your app for simulator and device targets. --- url: /docs/remote-cache/ios.md --- # iOS GitHub Action This GitHub Action allows you to build iOS apps using Rock's remote build system. It supports both simulator builds for development and signed device builds for testing and release. If you haven't yet, please check the [configuration guide](./github-actions-setup.md) where you can find information on optimal workflow setup, permissions, optimizations and GitHub Personal Access Tokens. ## Development Builds For Simulators Builds an APP (`.app`) file in debug configuration suitable for development. Doesn't require signing. Use in the GitHub Workflow file like this: ```yaml - name: Rock Remote Build - iOS simulator id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: simulator github-token: ${{ secrets.GITHUB_TOKEN }} configuration: Debug ``` ## Tester Builds For Devices Builds an IPA (`.ipa`) file in release variant suitable for testing. Requires signing. ### Prerequisites Signing an iOS app for distribution requires a certificate, a provisioning profile and an `ExportOptions.plist` file. #### ExportOptions.plist Archive and Export operations require an `ExportOptions.plist` file, which specifies the code signing settings for the `xcodebuild archive` and `xcodebuild -exportArchive` commands. If you don't have this file in your project, you can create it manually by exporting an archive from Xcode. Once finished, the output folder should contain an `ExportOptions.plist` file, which you should copy to your `ios/` folder and commit to your git repository. #### Manual Code Signing The easiest way to set up CI for iOS device builds is to use manual code signing. To set up manual code signing, ensure the following Xcode project settings: ``` Open Target => Signing & Capabilities => Release (tab) ``` Make sure that: - Automatic code signing is unticked - The Provisioning Profile is set to your distribution profile - Team & Signing certificate should indicate your designated team & certificate These correspond to the following \*.xcodeproj/project.pbxproj settings: - `CODE_SIGN_STYLE = Manual` - `"DEVELOPMENT_TEAM[sdk=iphoneos*]" = "[Your Apple Team ID]"` - `"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"` - `"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "[Your Provisioning Profile Name]"` Also, make sure that the `PRODUCT_BUNDLE_IDENTIFIER` is valid and globally unique (i.e., not used by any other app registered in any other Apple Developer account). #### Provisioning Profile Builds that are to be run on a device must be signed with a Development or Distribution provisioning profile. A Development profile allows for installing on a device through Xcode and is intended for development purposes. A Provisioning Profile, which can be thought of as a combination of a Certificate and an App ID, describes how the app is installed on a device and what devices are allowed to install it. There are the following types of profiles: - Development - Created with a Development certificate - Allows for installing on a device through Xcode - App Store - Created with a Distribution certificate - Allows for installing on a device through the App Store/TestFlight - Ad-Hoc - Created with a Distribution certificate - Allows for installing on devices registered with the given Apple Developer account and specified in the profile - Used for internal testing or small-scale distribution - Enterprise - Created with a Distribution certificate - Technically allows for installing on any device, but legally restricted to enterprise employees - This type of profile can be created only with an Apple Enterprise Developer account - Used for internal distribution and testing In order to install the provisioning profile on GitHub Actions, you need to export the provisioning profile as a base64 string, as described in the [GitHub docs](https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development). You can do it using the following command, which will copy the contents to your clipboard: ```bash base64 -i PROVISIONING_PROFILE.mobileprovision | pbcopy ``` Once created, store it as `ROCK_APPLE_PROVISIONING_PROFILE_BASE64` secret on your GitHub repository. #### Certificate A certificate can be either Development or Distribution. The Development certificate allows for installing on a device through Xcode and is intended for development purposes. The Distribution certificate allows for installing on a device through the App Store/TestFlight, etc. A certificate has a public and private part. The public part can generally be downloaded from the Apple Developer portal, while the private part is stored on a developer’s machine and needs to be distributed separately (e.g., to other developers or CI). :::info You need to have access to both the public and private parts of the certificate on the CI to generate an IPA file. ::: In order to install the certificate on GitHub Actions, you need to export the certificate (including the private key) as a base64 string, as described in the [GitHub docs](https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development). You can do it using the following command, which will copy the contents to your clipboard: ```bash base64 -i BUILD_CERTIFICATE.p12 | pbcopy ``` Once created, store it as `ROCK_APPLE_CERTIFICATE_BASE64` secret on your GitHub repository. :::note A modern alternative to the Distribution certificate is the "Distributed Managed" certificate, which is a managed certificate where the private part is stored on Apple’s servers, and Apple actually handles the signing operation. ::: #### GitHub Actions Secrets In order to build signed iOS device builds, you need to set up the following secrets on your GitHub repository: - `ROCK_APPLE_CERTIFICATE_BASE64` – Base64 version of the certificate - `ROCK_APPLE_CERTIFICATE_PASSWORD` – Certificate password - `ROCK_APPLE_PROVISIONING_PROFILE_BASE64` – Base64 version of the provisioning profile - `ROCK_APPLE_KEYCHAIN_PASSWORD` – Password to keychain (created temporarily by the GitHub Action) ### Running on GitHub Actions Use in the GitHub Workflow file like this: ```yaml - name: Rock Remote Build - iOS device id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: device github-token: ${{ secrets.GITHUB_TOKEN }} configuration: Release certificate-base64: ${{ secrets.APPLE_BUILD_CERTIFICATE_BASE64 }} certificate-password: ${{ secrets.APPLE_BUILD_CERTIFICATE_PASSWORD }} provisioning-profile-base64: ${{ secrets.APPLE_BUILD_PROVISIONING_PROFILE_BASE64 }} provisioning-profile-name: 'PROVISIONING_PROFILE_NAME' keychain-password: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} ``` Make sure that the `provisioning-profile-name` is the same as the one in your provisioning profile set in the Xcode project (see above). ### Other Action Inputs #### `rock-build-extra-params` Default: "" Pass extra parameters to the `rock build:ios` command, in order to apply custom code signing settings to the `xcodebuild archive` and `xcodebuild -exportArchive` commands. ```yaml - name: Rock Remote Build - iOS device id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: device github-token: ${{ secrets.GITHUB_TOKEN }} rock-build-extra-params: 'CUSTOM FLAGS AND ENVIRONMENT VARIABLES' ``` #### `re-sign` Default: `false` Re-sign the IPA with latest JS bytecode bundle with `rock sign:ios`. Necessary for tester device builds. When `true`, it will produce new artifact for every commit in a Pull Request, with a PR number appended to the original artifact name associated with native state of the app, e.g. `rock-ios-device-Release-94a82df39e12-1337`, where `1337` is the unique PR number. To avoid polluting artifact storage it will also handle removal of old artifacts associated with older commits. ```yaml - name: Rock Remote Build - iOS device id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: device github-token: ${{ secrets.GITHUB_TOKEN }} re-sign: true # ...rest of code signing inputs ``` #### `working-directory` Default: `.` When in monorepo, you may need to set the working directory something else than root of the repository. For example in the following setup: ``` packages/ mobile/ ios/ android/ rock.config.mjs ``` You'll need to set `working-directory: ./packages/mobile`: ```yaml - name: Rock Remote Build - iOS device id: rock-remote-build-ios uses: callstackincubator/ios@v3 with: destination: device github-token: ${{ secrets.GITHUB_TOKEN }} working-directory: ./packages/mobile ``` ### Action Outputs #### `artifact-url` URL of the relevant iOS build artifact. #### `artifact-id` ID of the relevant iOS build artifact. Suitable for retrieving artifacts for reuse in other jobs. ```yaml build-release: outputs: artifact-id: ${{ steps.rock-remote-build-ios.outputs.artifact-id }} # ...steps running action with `rock-remote-build-ios` id run-e2e-tests: runs-on: ubuntu-latest needs: build-release steps: - name: Download and Unpack IPA artifact run: | curl -L -H "Authorization: token ${{ github.token }}" -o artifact.zip "https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${{ needs.build-release.outputs.artifact-id }}/zip" unzip artifact.zip -d downloaded-artifacts ls -l downloaded-artifacts IPA_PATH=$(find downloaded-artifacts -name "*.ipa" -print -quit) echo "ARTIFACT_PATH_FOR_E2E=$IPA_PATH" >> $GITHUB_ENV shell: bash - name: Run E2E test run: # ...install $ARTIFACT_PATH_FOR_E2E on device and run tests ``` --- url: /docs/remote-cache/android.md --- # Android GitHub Action This GitHub Action allows you to build Android apps using Rock's remote build system. It supports both simulator builds for development and signed device builds for testing and release. If you haven't yet, please check the [configuration guide](./github-actions-setup.md) where you can find information on optimal workflow setup, permissions, optimizations and GitHub Personal Access Tokens. ## Development Builds For All Devices Builds an APK (`.apk`) file in debug variants suitable for development. Doesn't require signing. ### Running on GitHub Actions Use in the GitHub Workflow file like this: ```yaml - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: debug github-token: ${{ secrets.GITHUB_TOKEN }} ``` ## Tester Builds For All Devices Builds an APK file in release variants suitable for testing. Requires signing. ### Prerequisites To build release artifacts, you'll need to export the release.keystore as base64 string, e.g. using the following command: ```bash base64 -i release.keystore | pbcopy ``` On GitHub Actions secrets and variables page you'll need to set up the following secrets for your GitHub repository: - `KEYSTORE_BASE64` – Base64 version of the release keystore - `ROCK_UPLOAD_STORE_FILE` – Keystore store file name - `ROCK_UPLOAD_STORE_PASSWORD` – Keystore store password - `ROCK_UPLOAD_KEY_ALIAS` – Keystore key alias - `ROCK_UPLOAD_KEY_PASSWORD` – Keystore key password ### Running on GitHub Actions Use in the GitHub Workflow file like this: ```yaml - name: Rock Remote Build - Android device id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: release github-token: ${{ secrets.GITHUB_TOKEN }} # if you need to sign with non-debug keystore sign: true keystore-base64: ${{ secrets.KEYSTORE_BASE64 }} keystore-store-file: ${{ secrets.ROCK_UPLOAD_STORE_FILE }} keystore-store-password: ${{ secrets.ROCK_UPLOAD_STORE_PASSWORD }} keystore-key-alias: ${{ secrets.ROCK_UPLOAD_KEY_ALIAS }} keystore-key-password: ${{ secrets.ROCK_UPLOAD_KEY_PASSWORD }} ``` ### Other Action Inputs #### `rock-build-extra-params` Default: "" Pass extra parameters to the `rock build:android` command, in order to apply custom params for gradlew command. ```yaml - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: release github-token: ${{ secrets.GITHUB_TOKEN }} rock-build-extra-params: '--aab' # build an Android App Bundle for the Play Store ``` #### `re-sign` Default: `false` Re-sign the APK with latest JS bytecode bundle with `rock sign:android`. Necessary for tester device builds. When `true`, it will produce new artifact for every commit in a Pull Request, with a PR number appended to the original artifact name associated with native state of the app, e.g. `rock-android-release-9482df3912-1337`, where `1337` is the unique PR number. To avoid polluting artifact storage it will also handle removal of old artifacts associated with older commits. ```yaml - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: release github-token: ${{ secrets.GITHUB_TOKEN }} re-sign: true ``` #### `validate-gradle-wrapper` Default: `true` For security reasons we add Gradle Wrapper validation step to Android build action. Pass `false` to disable validation. ```yaml - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: debug github-token: ${{ secrets.GITHUB_TOKEN }} validate-gradle-wrapper: false ``` #### `working-directory` Default: `.` When in monorepo, you may need to set the working directory something else than root of the repository. For example in the following setup: ``` packages/ mobile/ ios/ android/ rock.config.mjs ``` You'll need to set `working-directory: ./packages/mobile`: ```yaml - name: Rock Remote Build - Android id: rock-remote-build-android uses: callstackincubator/android@v3 with: variant: debug github-token: ${{ secrets.GITHUB_TOKEN }} working-directory: ./packages/mobile ``` ### Action Outputs #### `artifact-url` URL of the relevant Android build artifact. #### `artifact-id` ID of the relevant Android build artifact. Suitable for retrieving artifacts for reuse in other jobs. ```yaml build-release: outputs: artifact-id: ${{ steps.rock-remote-build-android.outputs.artifact-id }} # ...steps running action with `rock-remote-build-android` id run-e2e-tests: runs-on: ubuntu-latest needs: build-release steps: - name: Download and Unpack APK artifact run: | curl -L -H "Authorization: token ${{ github.token }}" -o artifact.zip "https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${{ needs.build-release.outputs.artifact-id }}/zip" unzip artifact.zip -d downloaded-artifacts ls -l downloaded-artifacts APK_PATH=$(find downloaded-artifacts -name "*.apk" -print -quit) echo "ARTIFACT_PATH_FOR_E2E=$APK_PATH" >> $GITHUB_ENV shell: bash - name: Run E2E test run: # ...install $ARTIFACT_PATH_FOR_E2E on device and run tests ``` --- url: /docs/brownfield/intro.md --- # Integrating with Native Apps Rock, when extended with `@rock-js/plugin-brownfield-ios` and `@rock-js/plugin-brownfield-android` plugins and the [React Native Brownfield](https://github.com/callstack/react-native-brownfield) library, allows you to package all of your React Native code into native libraries (XCFramework for iOS, AAR for Android) that you can easily integrate into your existing iOS and Android apps. This "packaging approach," as we call it, allows us to achieve a radically simpler experience for native iOS and Android developers who want to integrate React Native into their apps. ![Packaging approach](./assets/packaging-approach.png) With this approach, there's **no need for you to**: - set up Node.js in your main app - configure CocoaPods for iOS - change your project directory structure - depend on React Native's own dependencies in your build tools ## Getting Started Choose your platform to begin: - [Android Integration](./android.md) - Add React Native to your Android app - [iOS Integration](./ios.mdx) - Add React Native to your iOS app --- url: /docs/brownfield/ios.md --- import { PackageManagerTabs } from '@theme'; # Integrating with iOS Apps Rock helps you package your React Native code into files that your iOS and Android apps can use. For iOS, it creates a `.xcframework` file that you can easily add to your app. To add React Native to your iOS app, we'll package your React Native code into an XCFramework. This way, you don't need to set up Node.js or CocoaPods in your main app. To make the integration simpler and more powerful, we'll use the [React Native Brownfield](https://github.com/callstack/react-native-brownfield) library. ## Creating a New Rock Project (Automated) If you are creating a new Rock project, you can select the `brownfield-ios` plugin: ``` > npm create rock ... ◆ What plugins do you want to start with? │ ◼ brownfield-ios ``` to add brownfield capabilities, install dependencies. Then jump to: - [6. Create the XCFramework](#6-create-the-xcframework). ## Integrating to An Existing Rock Project (Manual) If you have an existing Rock project, follow the instructions below. ### 1. Create a New Framework in React Native app's Xcode: 1. Open your React Native project's `ios/.xcworkspace` in Xcode 1. Add a new target by clicking File > New > Target 1. Choose the `Framework` template ![Framework Target](/brownfield_framework_target.png) 1. Give your framework a unique name. You'll use this name when adding it to your main app 1. Right-click the framework folder and select `Convert to Group`. CocoaPods doesn't work properly with references. Perform this step for both `` and `Tests` folders. ![The menu that appears when user right clicks on the generated framework folder](/brownfield_convert_to_group.png) 1. Set these build settings for your framework: | Build Setting | Value | What it does | | -------------------------------- | ----- | ------------------------------------------------------------------------------------------------- | | Build Libraries for Distribution | YES | Creates a module interface for Swift. Also checks if the framework works with your Xcode version. | | User Script Sandboxing | NO | Lets scripts modify files, which we need to create the JavaScript bundle. | | Skip Install | NO | Makes sure Xcode creates the framework files we need. | | Enable Module Verifier | NO | Skips testing the framework during build, which makes builds faster. | ### 2. Update CocoaPods in your React Native app: 1. Add your new framework to `ios/Podfile`: ```ruby title="Podfile" {3-5} target '' do # Add these lines target '' do inherit! :complete end end ``` ### 3. Add the Bundle Script: 1. In Xcode, click on your React Native app target 1. Go to Build Phases 1. Find the `Bundle React Native code and images` step ![Bundle React Native code and images build phase](/bundle_phase.png) 1. Copy the script from there 1. Click on your framework target 1. Go to Build Phases 1. Click the + button and choose `New Run Script Phase` ![New Run Script Phase](/new_run_script.png) 1. Paste the script you copied 1. Name the phase `Bundle React Native code and images` 1. Add these files to the script's input files: - `$(SRCROOT)/.xcode.env.local` - `$(SRCROOT)/.xcode.env` ### 4. Create the Framework's Public Interface: 1. Install React Native Brownfield library: 1. Create a new Swift file in your framework folder using Xcode with the following contents: ```swift // Export helpers from @callstack/react-native-brownfield library @_exported import ReactBrownfield // Initializes a Bundle instance that points at the framework target. public let ReactNativeBundle = Bundle(for: InternalClassForBundle.self) class InternalClassForBundle {} ``` ### 5. Setup Rock's brownfield plugin: 1. Add `@rock-js/plugin-brownfield-ios` to your React Native project 1. Add this to your `rock.config.mjs`: ```js title="rock.config.mjs" import { pluginBrownfieldIos } from '@rock-js/plugin-brownfield-ios'; export default { plugins: [ pluginBrownfieldIos(), // ... ], }; ``` ### 6. Create the XCFramework Run the following command in Terminal to compile frameworks for Hermes, React Native Brownfield library, and your app: ```sh title="Terminal" rock package:ios --scheme --configuration Release ``` :::warning If you want to run the React Native app independently from the native iOS app, you'll still need to run the `package:ios` command and add the `ReactBrownfield.xcframework` to the project, as instructed in [step 7](#7-add-the-framework-to-your-ios-app). Otherwise, the `run:ios` command will fail with: ``` error: underlying Objective-C module 'ReactBrownfield' not found ``` ::: ### 7. Add the Framework to Your iOS App: 1. Open the `.rock/cache/ios/package` directory and drag the following files into your app's Xcode project (you can select all at once): ``` .rock/cache/ios/package ├── hermesvm.xcframework # JavaScript runtime (hermes.xcframework in React Native 0.81 or lower) ├── ReactBrownfield.xcframework # React Native Brownfield library └── .xcframework # Your framework target ``` ![Linked frameworks in Xcode sidebar](/frameworks_sidebar.png) :::info Xcode builds depend on files being referenced in the project file. If you create a new source file without using Xcode, your file is not going to be referenced by the project. ::: 1. Initialize React Native in `AppDelegate.swift` and show it using `ReactNativeViewController` available from React Native Brownfield library: ```swift title="AppDelegate.swift" {6-9} import @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize React Native ReactNativeBrownfield.shared.bundle = ReactNativeBundle ReactNativeBrownfield.shared.startReactNative(onBundleLoaded: { print("React Native bundle loaded") }, launchOptions: launchOptions) // Add `window` property required by React Native window = UIWindow(frame: UIScreen.main.bounds) // Create VC that calls your module by name registered by `AppRegistry.registerComponent` of your React Native app let reactNativeVC = ReactNativeViewController(moduleName: "ReactNativeApp") // Display the view as full window or anyhow you need window?.rootViewController = reactNativeVC window?.makeKeyAndVisible() } ``` :::note Instead of using `AppDelegate` you can also use `SceneDelegate` ::: ### 8. Run your native iOS App Now that you have everything set up, you can run your app in Debug or Release configuration, and it will display a React Native app we just packaged with `package:ios` command. #### Debug configuration When running in Debug, React Native Brownfield will expect you to run a JS dev server. You can do so by running `start` command in your Terminal app: ```shell npx rock start ``` #### Release configuration When running in Release, React Native Brownfield will load the JS bundle (`main.jsbundle`) directly from the release XCFramework file without requiring you to run any JavaScript tooling. --- url: /docs/brownfield/android.md --- import { PackageManagerTabs } from '@theme'; # Integrating with Android Apps Rock helps you package your React Native code into files that your iOS and Android apps can use. For Android, it creates an `.aar` file that you can easily add to your app. To add React Native to your Android app, we'll package your React Native code into an AAR. This way, you don't need to set up Node.js in your main app. Here's how to do it: ## Creating a New Rock Project (Automated) If you are creating a new Rock project, you can select the `brownfield-android` plugin: ``` > npm create rock ... ◆ What plugins do you want to start with? │ ◼ brownfield-android ``` to add brownfield capabilities, install dependencies. Then jump to: - [9. Create the AAR](#9-create-the-aar) ## Integrating to An Existing Rock Project (Manual) If you have an existing Rock project, follow the instructions below. ### 1. Create a New Android Library Module First, we'll create a new Android Library module in your React Native project. This module will contain your React Native UI and provide APIs for loading it in your native Android app. 1. Open your React Native project's `android` folder in Android Studio 1. Go to `File → New Module → Android Library` and create a new module: ![Creating a new Android Library module named reactnativeapp](./assets/create_module.png) :::warning Module Naming For the sake of this tutorial we use module name `reactnativeapp` in the `com.yourapp` app. Please, adjust the name of the app and the module to your preferences and make sure to update the code snippets presented below accordingly. ::: 1. After the sync completes, run your React Native app to make sure everything works 1. Test the build by running `./gradlew assembleRelease` in the android directory ### 2. Set Up the AAR Gradle Plugin We need a special Gradle plugin to create an AAR that includes all dependencies. We'll use the [`brownfield-gradle-plugin`](https://github.com/callstack/react-native-brownfield/tree/main/gradle-plugins/react). 1. Add the gradle plugin dependency to your `android/build.gradle`: ```groovy title="android/build.gradle" {3-10,15} buildscript { repositories { google() mavenCentral() } dependencies { classpath("com.callstack.react:brownfield-gradle-plugin:0.6.2") // check the latest version: https://mvnrepository.com/artifact/com.callstack.react/brownfield-gradle-plugin } } ``` 1. Add the plugin to your `reactnativeapp/build.gradle.kts`: ```groovy title="reactnativeapp/build.gradle.kts" {5} plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("com.facebook.react") id("com.callstack.react.brownfield") } ``` 1. Add autolinking setup to the `react` block in `reactnativeapp/build.gradle.kts`: ```groovy title="reactnativeapp/build.gradle.kts" {1,3} react { autolinkLibrariesWithApp() } ``` After adding these, sync your project and run `./gradlew assembleRelease` to verify everything works. ### 3. Add React Native Dependencies Add the required React Native dependencies to your `reactnativeapp/build.gradle.kts`: ```groovy title="reactnativeapp/build.gradle.kts" {2-3} dependencies { // Match your version of React Native, here 0.86: api("com.facebook.react:react-android:0.86.0") // For React Native 0.86 or newer use: api("com.facebook.hermes:hermes-android:250829098.0.14") } ``` After adding these, sync your project and run `./gradlew assembleRelease` to verify everything works. ### 4. Add React Native Brownfield Here we add the `react-native-brownfield` library to help us with APIs required to initialize and present react-native views. **Compatibility Matrix** | React Native Version | `@callstack/react-native-brownfield` Version | | -------------------- | -------------------------------------------- | | 0.81+ | v2 (latest) | | < 0.81 | v1.2.0 | Our default brownfield template uses the latest version of `@callstack/react-native-brownfield` (currently v2). ### 5. Create React Native Host Manager Create a new file called `ReactNativeHostManager.kt` in your `reactnativeapp` module: :::info The `loadReactNative` call is only required if you're on React Native version >= 0.80.0. If you're on lower version, skip it. ::: ```kotlin package com.yourapp.reactnativeapp // If you used a different package name when creating the library, change it here import android.app.Application import com.callstack.reactnativebrownfield.OnJSBundleLoaded import com.callstack.reactnativebrownfield.ReactNativeBrownfield import com.facebook.react.PackageList import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { loadReactNative(application) // Only required if you're on RN version >= 0.80.0 val packageList = PackageList(application).packages ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded) } } ``` Here, we wrap the `react-native-brownfield` API in our own `ReactNativeHostManager` so that the native App only have to pass an `Application` instance and not interact with any react-native API directly. [See more](https://github.com/callstack/react-native-brownfield/blob/main/docs/GUIDELINES.md) ### 6. Populate Build Config: Add build configuration fields: ```groovy title="reactnativeapp/build.gradle.kts" {4-5} android { defaultConfig { minSdk = 24 buildConfigField("boolean", "IS_EDGE_TO_EDGE_ENABLED", properties["edgeToEdgeEnabled"].toString()) buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", properties["newArchEnabled"].toString()) buildConfigField("boolean", "IS_HERMES_ENABLED", properties["hermesEnabled"].toString()) } publishing { multipleVariants { allVariants() } } } ``` The template defaults `newArchEnabled=true` in `android/gradle.properties`. Set it to `false` if you need to opt out of the New Architecture. ### 7. Configure Maven Publishing Add the Maven publish plugin to your `reactnativeapp/build.gradle.kts`: ```groovy title="reactnativeapp/build.gradle.kts" {6} plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("com.facebook.react") id("com.callstack.react.brownfield") `maven-publish` } ``` Configure the publishing settings: ```groovy title="reactnativeapp/build.gradle.kts" import groovy.json.JsonOutput import groovy.json.JsonSlurper publishing { publications { create("mavenAar") { groupId = "com.yourapp" artifactId = "reactnativeapp" version = "0.0.1-local" afterEvaluate { from(components.getByName("default")) } pom { withXml { /** * As a result of `from(components.getByName("default")` all of the project * dependencies are added to `pom.xml` file. We do not need the react-native * third party dependencies to be a part of it as we embed those dependencies. */ val dependenciesNode = (asNode().get("dependencies") as groovy.util.NodeList).first() as groovy.util.Node dependenciesNode.children() .filterIsInstance() .filter { (it.get("groupId") as groovy.util.NodeList).text() == rootProject.name } .forEach { dependenciesNode.remove(it) } } } } } repositories { mavenLocal() // Publishes to the local Maven repository (~/.m2/repository by default) } } val moduleBuildDir: Directory = layout.buildDirectory.get() /** * As a result of `from(components.getByName("default")` all of the project * dependencies are added to `module.json` file. We do not need the react-native * third party dependencies to be a part of it as we embed those dependencies. */ tasks.register("removeDependenciesFromModuleFile") { doLast { file("$moduleBuildDir/publications/mavenAar/module.json").run { val json = inputStream().use { JsonSlurper().parse(it) as Map } (json["variants"] as? List>)?.forEach { variant -> (variant["dependencies"] as? MutableList>)?.removeAll { it["group"] == rootProject.name } } writer().use { it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json))) } } } } tasks.named("generateMetadataFileForMavenAarPublication") { finalizedBy("removeDependenciesFromModuleFile") } ``` ### 8. Set up Rock for AAR generation :::warning If you're integrating an Expo app with Expo CLI instead of Rock, skip this step. ::: 1. Add `@rock-js/plugin-brownfield-android` to your dependencies 1. Update your `rock.config.mjs`: ```js title="rock.config.mjs" import { pluginBrownfieldAndroid } from '@rock-js/plugin-brownfield-android'; export default { plugins: [pluginBrownfieldAndroid()], }; ``` ### 9. Create the AAR :::warning If you're integrating an Expo app with Expo CLI instead of Rock, skip this step. ::: 1. Run this command to generate the final AAR: ```sh title="Terminal" rock package:aar --variant Release --module-name reactnativeapp ``` 1. Once the AAR is created, publish it to local Maven registry to be consumable by the native app: ```sh title="Terminal" rock publish-local:aar --module-name reactnativeapp ``` ### 10. Extra steps for Expo CLI and Expo Modules 1. Make `ReactNativeHostManager` aware with expo modules: ```diff @@ -3,13 +3,30 @@ +import android.app.Application +import android.content.res.Configuration import com.callstack.reactnativebrownfield.OnJSBundleLoaded import com.callstack.reactnativebrownfield.ReactNativeBrownfield import com.facebook.react.PackageList +import com.facebook.react.ReactHost import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ExpoReactHostFactory object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { loadReactNative(application) - val packageList = PackageList(application).packages - ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded) + ApplicationLifecycleDispatcher.onApplicationCreate(application) + + val reactHost: ReactHost by lazy { + ExpoReactHostFactory.getDefaultReactHost( + context = application.applicationContext, + packageList = PackageList(application).packages, + ) + } + + ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded) } + + fun onConfigurationChanged(application: Application, newConfig: Configuration) { + ApplicationLifecycleDispatcher.onConfigurationChanged(application, newConfig) + } } ``` 2. Update your `reactnativeapp/build.gradle`: ```diff + reactBrownfield { /** * This is available from `com.callstack.react.brownfield` version > 0.3.0 * It takes care of linking expo dependencies like expo-image with your AAR module. * * Default value is false. */ + isExpo = true + } react { autolinkLibrariesWithApp() } @@ -76,6 +76,18 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") } /** * This function is used in the places where we: * * Remove the `expo` dependency from the `module.json` and `pom.xml file. Otherwise, the * gradle will try to resolve this and will throw an error, since this dependency won't * be available from a remote repository. * * Your AAR does not need this dependency. */ + fun isExpoArtifact(group: String, artifactId: String): Boolean { + return group == "host.exp.exponent" && artifactId == "expo" + } publishing { publications { @@ -97,7 +109,12 @@ publishing { val dependenciesNode = (asNode().get("dependencies") as groovy.util.NodeList).first() as groovy.util.Node dependenciesNode.children() .filterIsInstance() - .filter { (it.get("groupId") as groovy.util.NodeList).text() == rootProject.name } + .filter { + val artifactId = (it["artifactId"] as groovy.util.NodeList).text() + val group = (it["groupId"] as groovy.util.NodeList).text() + + (isExpoArtifact(group, artifactId) || group == rootProject.name) + } .forEach { dependenciesNode.remove(it) } } } @@ -121,7 +138,12 @@ tasks.register("removeDependenciesFromModuleFile") { file("$moduleBuildDir/publications/mavenAar/module.json").run { val json = inputStream().use { JsonSlurper().parse(it) as Map } (json["variants"] as? List>)?.forEach { variant -> - (variant["dependencies"] as? MutableList>)?.removeAll { it["group"] == rootProject.name } + (variant["dependencies"] as? MutableList>)?.removeAll { + val module = it["module"] as String + val group = it["group"] as String + + (isExpoArtifact(group, module) || group == rootProject.name) + } } writer().use { it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json))) } } ``` That is all you need to change. Step 8.i is not required with Expo, you can skip it. See Step 8.ii to generate AAR. 3. Generate AAR when using Expo Here you can see how to generate AAR. The link uses a script which first builds the AAR and then publish to to `mavenLocal`. [see here](https://github.com/callstackincubator/modern-brownfield-ref/blob/b23641f3ff7c278743d35013841d5494905ba190/package.json#L14) ### 11. Add the AAR to Your Android App > Note: You'll need an existing Android app or create a new one in Android Studio. 1. Add `mavenLocal()` to your app's dependency resolution in `settings.gradle.kts`: ```groovy title="settings.gradle.kts" {3} dependencyResolutionManagement { repositories { mavenLocal() } } ``` 2. Add the dependency to your app's `build.gradle.kts`: ```groovy title="build.gradle.kts" {2} dependencies { implementation("com.yourapp:reactnativeapp:0.0.1-local") } ``` 3. Initialize React Native in your `MainActivity`: ```kotlin import com.yourapp.reactnativeapp.ReactNativeHostManager class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ReactNativeHostManager.initialize(this.application) { println("JS bundle loaded") } // ... rest of your onCreate code } } ``` ### 12. Show the React Native UI Add a button to your `activity_main.xml`: ```xml