diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d0e78bad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "json.schemas": [ + { + "fileMatch": ["**/package.json"], + "url": "https://oss.callstack.com/react-native-brownfield/package-json.schema.json" + } + ] +} \ No newline at end of file diff --git a/apps/ExpoApp54/package.json b/apps/ExpoApp54/package.json index e40f5430..c6e76d1b 100644 --- a/apps/ExpoApp54/package.json +++ b/apps/ExpoApp54/package.json @@ -12,9 +12,9 @@ "test": "jest --config jest.config.js", "prebuild": "expo prebuild", "brownfield:prepare:android:ci": "cd .. && node --experimental-strip-types --no-warnings ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp54", - "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release --verbose", - "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib --verbose", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release --verbose", + "brownfield:package:android": "brownfield package:android --variant release", + "brownfield:publish:android": "brownfield publish:android", + "brownfield:package:ios": "brownfield package:ios --configuration Release", "eas:stg": "EXPO_TOKEN=$EAS_TOKEN eas update --channel production --message 'testing 1st stg channel update' --platform android" }, "dependencies": { @@ -54,9 +54,5 @@ "jest-expo": "~54.0.16", "react-test-renderer": "19.1.0", "typescript": "~5.9.3" - }, - "brownie": { - "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp54/Generated/", - "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp54" } } diff --git a/apps/ExpoApp54/react-native-brownfield.config.json b/apps/ExpoApp54/react-native-brownfield.config.json new file mode 100644 index 00000000..d11c47e8 --- /dev/null +++ b/apps/ExpoApp54/react-native-brownfield.config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "moduleName": "brownfieldlib", + "scheme": "BrownfieldLib", + "verbose": true, + "brownie": { + "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp54/Generated/", + "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp54" + } +} diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index af003688..c033f68a 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -11,9 +11,9 @@ "test": "jest --config jest.config.js", "prebuild": "expo prebuild", "brownfield:prepare:android:ci": "cd .. && node --experimental-strip-types --no-warnings ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp55", - "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release --verbose", - "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib --verbose", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release --verbose", + "brownfield:package:android": "brownfield package:android --variant release", + "brownfield:publish:android": "brownfield publish:android", + "brownfield:package:ios": "brownfield package:ios --configuration Release", "eas:stg": "EXPO_TOKEN=$EAS_TOKEN eas update --channel production --message 'testing 1st stg channel update' --platform ios --environment staging" }, "dependencies": { @@ -62,8 +62,14 @@ "typescript": "~5.9.2" }, "private": true, - "brownie": { - "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", - "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp55" + "react-native-brownfield": { + "$schema": "../../packages/cli/schema.json", + "brownie": { + "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", + "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp55" + }, + "moduleName": "brownfieldlib", + "scheme": "BrownfieldLib", + "verbose": true } } diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json index ee760f79..f9f4d7b0 100644 --- a/apps/RNApp/package.json +++ b/apps/RNApp/package.json @@ -7,9 +7,9 @@ "ios": "react-native run-ios", "build:example:android-rn": "react-native build-android", "build:example:ios-rn": "react-native build-ios", - "brownfield:package:android": "brownfield package:android --module-name :BrownfieldLib --variant release --verbose", - "brownfield:publish:android": "brownfield publish:android --module-name :BrownfieldLib --verbose", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release --verbose", + "brownfield:package:android": "brownfield package:android --variant release", + "brownfield:publish:android": "brownfield publish:android", + "brownfield:package:ios": "brownfield package:ios --configuration Release", "lint": "eslint .", "start": "react-native start", "test": "jest --config jest.config.js", diff --git a/apps/RNApp/react-native-brownfield.config.js b/apps/RNApp/react-native-brownfield.config.js new file mode 100644 index 00000000..051eec50 --- /dev/null +++ b/apps/RNApp/react-native-brownfield.config.js @@ -0,0 +1,8 @@ +/** + * @type {import('@callstack/react-native-brownfield').BrownfieldConfig} + */ +module.exports = { + moduleName: ':BrownfieldLib', + scheme: 'BrownfieldLib', + verbose: true, +}; diff --git a/docs/docs/docs/api-reference/_meta.json b/docs/docs/docs/api-reference/_meta.json index 01a36bfd..cb0953b0 100644 --- a/docs/docs/docs/api-reference/_meta.json +++ b/docs/docs/docs/api-reference/_meta.json @@ -1,4 +1,9 @@ [ + { + "type": "file", + "name": "configuration", + "label": "Configuration files" + }, { "type": "dir", "name": "react-native-brownfield", diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx new file mode 100644 index 00000000..ed4a3126 --- /dev/null +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -0,0 +1,194 @@ +# Configuration files + +The Brownfield CLI can load configuration from a file instead of repeating the same flags on every command. +That configuration covers both `@callstack/react-native-brownfield` and `@callstack/brownie` options. + +Configuration keys use camelCase names that match CLI flags. +For example, `--module-name` becomes `moduleName`, `--build-folder` becomes `buildFolder`, and `--use-prebuilt-rn-core` becomes `usePrebuiltRnCore`. + +## Choose one configuration source + +The CLI supports exactly one configuration source per project: + +- `react-native-brownfield.config.js` +- `react-native-brownfield.config.json` +- `package.json` under the `react-native-brownfield` key + +Do not keep more than one of these at the same time. +If the CLI finds multiple sources, it throws an error instead of guessing which one should win. + +When both a config value and a CLI flag are set for the same option, the CLI flag wins. +The CLI also validates the file against the published schema and logs warnings for unknown or invalid keys. + +## JavaScript config file + +If you prefer a JavaScript file, create `react-native-brownfield.config.js` and export a plain object with `module.exports`: + +```js +/** @type {import('@callstack/react-native-brownfield').BrownfieldConfig} */ +module.exports = { + moduleName: ':BrownfieldLib', + scheme: 'BrownfieldLib', + verbose: true, + brownie: { + kotlin: + './android/BrownfieldLib/src/main/java/com/example/brownfield/Generated/', + kotlinPackageName: 'com.example.brownfield', + }, +}; +``` + +## JSON config file + +If you want schema autocomplete and validation directly in the config file, use `react-native-brownfield.config.json`: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "moduleName": "brownfieldlib", + "scheme": "BrownfieldLib", + "configuration": "Release", + "verbose": true, + "usePrebuiltRnCore": true, + "brownie": { + "kotlin": "./android/brownfieldlib/src/main/java/com/example/brownfield/Generated/", + "kotlinPackageName": "com.example.brownfield" + } +} +``` + +## package.json config + +If you prefer to keep everything in `package.json`, place the configuration under `react-native-brownfield`: + +```json +{ + "name": "my-app", + "react-native-brownfield": { + "moduleName": ":BrownfieldLib", + "scheme": "BrownfieldLib", + "verbose": true, + "brownie": { + "kotlin": "./android/BrownfieldLib/src/main/java/com/example/brownfield/Generated/", + "kotlinPackageName": "com.example.brownfield" + } + } +} +``` + +## Configuration reference + +All file-based options mirror CLI flags, but they use camelCase property names. + +### Shared keys + +| Key | Type | Description | +| --------- | --------- | --------------------------------------------------------------------- | +| `$schema` | `string` | JSON Schema URL used by editors for validation and autocomplete. | +| `verbose` | `boolean` | Enables verbose CLI logging. | +| `brownie` | `object` | Nested Brownie configuration used by `brownfield codegen`. See below. | + +### Android keys + +| Key | Type | Description | +| ------------ | -------- | -------------------------------------------------------------------- | +| `moduleName` | `string` | Android module name used for packaging and publishing AAR artifacts. | +| `variant` | `string` | Android build variant, for example `debug` or `freeRelease`. | + +### iOS keys + +| Key | Type | Description | +| -------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- | +| `scheme` | `string` | Xcode scheme used for packaging. | +| `configuration` | `string` | Xcode build configuration, for example `Debug` or `Release`. | +| `target` | `string` | Explicit Xcode target name. | +| `destination` | `string[]` | One or more Xcode destinations, such as `simulator`, `device`, or full destination strings. | +| `buildFolder` | `string` | Custom build output directory. By default, Brownfield uses the `.brownfield/build` path inside the iOS project. | +| `archive` | `boolean` | Creates an archive build suitable for IPA export and distribution. | +| `extraParams` | `string[]` | Extra arguments passed to `xcodebuild`. | +| `exportExtraParams` | `string[]` | Extra arguments passed to the archive export step. | +| `exportOptionsPlist` | `string` | Export options plist filename used during archive export. | +| `installPods` | `boolean` | Controls automatic CocoaPods installation. Set `false` to match `--no-install-pods`. | +| `newArch` | `boolean` | Controls React Native new architecture support. Set `false` to match `--no-new-arch`. | +| `local` | `boolean` | Forces a local `xcodebuild` flow. | +| `usePrebuiltRnCore` | `boolean` | Controls whether iOS packaging uses React Native Apple prebuilts. Omit it to keep Brownfield's version-aware defaults. | + +## Brownie configuration + +The Brownie configuration lives inside the main Brownfield config under the `brownie` key. +This is the preferred format for Brownie code generation. + +Currently supported Brownie keys are: + +| Key | Type | Description | +| ------------------- | -------- | ----------------------------------------------------------------------- | +| `kotlin` | `string` | Directory where generated Kotlin Brownie store files should be written. | +| `kotlinPackageName` | `string` | Kotlin package name used in generated Brownie store files. | + +Example inside `react-native-brownfield.config.json`: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "brownie": { + "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", + "kotlinPackageName": "com.rnapp.brownfieldlib" + } +} +``` + +Only the Kotlin output is configurable. +Swift Brownie files are always generated to `node_modules/@callstack/brownie/ios/Generated/`. + +## Migrating from legacy Brownie configuration + +Legacy Brownie configuration used a top-level `brownie` block in `package.json`: + +```json +{ + "brownie": { + "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", + "kotlinPackageName": "com.rnapp.brownfieldlib" + } +} +``` + +The new format moves the same values under the main Brownfield config: + +```json +{ + "react-native-brownfield": { + "moduleName": ":BrownfieldLib", + "scheme": "BrownfieldLib", + "brownie": { + "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", + "kotlinPackageName": "com.rnapp.brownfieldlib" + } + } +} +``` + +You can also migrate to a standalone config file: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "moduleName": ":BrownfieldLib", + "scheme": "BrownfieldLib", + "brownie": { + "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", + "kotlinPackageName": "com.rnapp.brownfieldlib" + } +} +``` + +Migration steps: + +1. Pick one main Brownfield config source. +2. Move the legacy `package.json#brownie` values into the nested `brownie` object in that source. +3. Remove the old top-level `brownie` block from `package.json`. +4. Run `brownfield codegen` again. + +Do not keep the legacy and new Brownie configuration at the same time. +If both are present, `brownfield codegen` throws an error. +If only the legacy format is present, the command still works for now, but it prints a migration warning. diff --git a/docs/docs/docs/cli/brownfield.mdx b/docs/docs/docs/cli/brownfield.mdx index 1e029a30..c323ca3a 100644 --- a/docs/docs/docs/cli/brownfield.mdx +++ b/docs/docs/docs/cli/brownfield.mdx @@ -2,6 +2,11 @@ The `brownfield` CLI provides utilities for building & packaging artifacts for brownfield projects that use the `@callstack/react-native-brownfield` library. +:::tip Configuration file +You can store supported `brownfield` CLI options in a project configuration file instead of passing the same flags on every command. +See [Configuration files](/docs/api-reference/configuration) for supported config sources and option names. +::: + ## Usage ```bash @@ -36,7 +41,6 @@ Available arguments: | --no-install-pods | Skip automatic CocoaPods installation | | --no-new-arch | Run React Native in legacy async architecture | | --local | Force local build with xcodebuild | -| --verbose | Enable verbose logging | The build directory will be placed in the `/.brownfield/build` folder by default and the build outputs (XCFrameworks) will be created in the `/.brownfield/package/build` folder: diff --git a/docs/docs/docs/cli/brownie.mdx b/docs/docs/docs/cli/brownie.mdx index a2a01dab..201297f8 100644 --- a/docs/docs/docs/cli/brownie.mdx +++ b/docs/docs/docs/cli/brownie.mdx @@ -2,6 +2,11 @@ The `brownfield codegen` CLI command generates `@callstack/brownie` (Brownie) state management library native store types from TypeScript schema. +:::tip Configuration file +You can configure Brownie codegen from the main Brownfield config file by using the nested `brownie` object. +See [Configuration files](/docs/api-reference/configuration) for supported config sources and Brownie-specific settings. +::: + ## Usage ```bash diff --git a/docs/docs/docs/getting-started/android.mdx b/docs/docs/docs/getting-started/android.mdx index dab37ac1..7bea9c3f 100644 --- a/docs/docs/docs/getting-started/android.mdx +++ b/docs/docs/docs/getting-started/android.mdx @@ -281,21 +281,38 @@ tasks.named("generateMetadataFileForMavenAarPublication") { } ``` -## 7. Create the AAR +## 7. Create a Brownfield Configuration + +Create `react-native-brownfield.config.json` in your project root: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "moduleName": "reactnativeapp", + "variant": "Release" +} +``` + +This lets the CLI reuse your packaging settings without repeating the same flags on every command. +See [Configuration files](/docs/api-reference/configuration) for JavaScript and `package.json` variants and the full list of supported options. + +## 8. Create the AAR Use the brownfield CLI to package your React Native app: ```bash -npx brownfield package:android --variant Release --module-name reactnativeapp +npx brownfield package:android ``` Then publish to **local Maven**: ```bash -npx brownfield publish:android --module-name reactnativeapp +npx brownfield publish:android ``` -## 8. Add the AAR to Your Android App +If you prefer to keep the settings on the command line, you can still run `npx brownfield package:android --variant Release --module-name reactnativeapp` and `npx brownfield publish:android --module-name reactnativeapp` instead. + +## 9. Add the AAR to Your Android App Add **`mavenLocal()`** to your app's `settings.gradle.kts`: @@ -315,7 +332,7 @@ dependencies { } ``` -## 9. Initialize React Native +## 10. Initialize React Native In your **`MainActivity`**: @@ -333,7 +350,7 @@ class MainActivity : AppCompatActivity() { } ``` -## 10. Show the React Native UI +## 11. Show the React Native UI ### Using Fragment diff --git a/docs/docs/docs/getting-started/ios.mdx b/docs/docs/docs/getting-started/ios.mdx index a5e1e70a..025dac86 100644 --- a/docs/docs/docs/getting-started/ios.mdx +++ b/docs/docs/docs/getting-started/ios.mdx @@ -105,17 +105,34 @@ public let ReactNativeBundle = Bundle(for: InternalClassForBundle.self) class InternalClassForBundle {} ``` -## 5. Create the XCFramework +## 5. Create a Brownfield Configuration + +Create `react-native-brownfield.config.json` in your project root: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "scheme": "", + "configuration": "Release" +} +``` + +This lets the CLI reuse your packaging settings without repeating the same flags on every command. +See [Configuration files](/docs/api-reference/configuration) for JavaScript and `package.json` variants and the full list of supported options. + +## 6. Create the XCFramework Use the brownfield CLI to package your React Native app: ```bash -npx brownfield package:ios --scheme --configuration Release +npx brownfield package:ios ``` This creates the XCFramework in **`ios/.brownfield/package/build/`** (relative to your project root). -## 6. Add the Framework to Your iOS App +If you prefer to keep the settings on the command line, you can still run `npx brownfield package:ios --scheme --configuration Release` instead. + +## 7. Add the Framework to Your iOS App 1. Open **`ios/.brownfield/package/build`** directory (relative to your React Native project root) 2. Drag these files into your native iOS app's Xcode project: @@ -128,7 +145,7 @@ This creates the XCFramework in **`ios/.brownfield/package/build/`** (relative t ![Frameworks in Xcode Sidebar](/images/frameworks.png) -## 7. Initialize React Native +## 8. Initialize React Native In your native iOS app's **`AppDelegate.swift`**: @@ -162,7 +179,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } ``` -## 8. Run Your App +## 9. Run Your App ### Debug Configuration diff --git a/docs/docs/docs/getting-started/quick-start.mdx b/docs/docs/docs/getting-started/quick-start.mdx index 13499245..e73f47af 100644 --- a/docs/docs/docs/getting-started/quick-start.mdx +++ b/docs/docs/docs/getting-started/quick-start.mdx @@ -77,6 +77,7 @@ Now that you have the library installed, follow the platform-specific guides to For detailed API documentation, see: +- [Configuration files](/docs/api-reference/configuration) - [Swift API](/docs/api-reference/react-native-brownfield/swift) - [Objective-C API](/docs/api-reference/react-native-brownfield/objective-c) - [Kotlin API](/docs/api-reference/react-native-brownfield/kotlin) diff --git a/docs/docs/public/package-json.schema.json b/docs/docs/public/package-json.schema.json new file mode 100644 index 00000000..6eb5552f --- /dev/null +++ b/docs/docs/public/package-json.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "React Native Brownfield package.json extension", + "description": "Adds react-native-brownfield configuration completions to package.json.", + "type": "object", + "properties": { + "react-native-brownfield": { + "$ref": "https://oss.callstack.com/react-native-brownfield/schema.json" + } + } +} \ No newline at end of file diff --git a/docs/docs/public/schema.json b/docs/docs/public/schema.json new file mode 100644 index 00000000..93f25766 --- /dev/null +++ b/docs/docs/public/schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "React Native Brownfield CLI config", + "description": "Configuration for react-native-brownfield.config.json and package.json#react-native-brownfield.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for editor tooling" + }, + "archive": { + "type": "boolean", + "description": "Create an Xcode archive (IPA) of the build, required for uploading to App Store Connect or distributing to TestFlight." + }, + "buildFolder": { + "type": "string", + "description": "Location for iOS build artifacts. Corresponds to Xcode's \"-derivedDataPath\". By default, the '/.brownfield/build' path is used." + }, + "brownie": { + "type": "object", + "description": "Configuration for Brownie code generation. Use this nested object in react-native-brownfield config instead of the legacy package.json#brownie block.", + "additionalProperties": false, + "properties": { + "kotlin": { + "type": "string", + "description": "Directory where generated Kotlin Brownie store files should be written." + }, + "kotlinPackageName": { + "type": "string", + "description": "Kotlin package name used in generated Brownie store files." + } + } + }, + "configuration": { + "type": "string", + "description": "Explicitly set the scheme configuration to use. This option is case sensitive." + }, + "destination": { + "type": "array", + "description": "Define destination values for the build. You can pass multiple destinations as separate values. Supported values include \"simulator\", \"device\", or full xcodebuild destination strings.", + "items": { + "type": "string" + } + }, + "exportExtraParams": { + "type": "array", + "description": "Custom params passed to the xcodebuild export archive command.", + "items": { + "type": "string" + } + }, + "exportOptionsPlist": { + "type": "string", + "description": "Name of the export options file for archiving. Defaults to ExportOptions.plist." + }, + "extraParams": { + "type": "array", + "description": "Custom params passed to the xcodebuild command.", + "items": { + "type": "string" + } + }, + "installPods": { + "type": "boolean", + "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." + }, + "local": { + "type": "boolean", + "description": "Force a local build with xcodebuild." + }, + "moduleName": { + "type": "string", + "description": "AAR module name." + }, + "newArch": { + "type": "boolean", + "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." + }, + "usePrebuiltRnCore": { + "type": "boolean", + "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." + }, + "scheme": { + "type": "string", + "description": "Explicitly set the Xcode scheme to use." + }, + "target": { + "type": "string", + "description": "Explicitly set the Xcode target to use." + }, + "variant": { + "type": "string", + "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." + }, + "verbose": { + "type": "boolean", + "description": "Enable verbose logging." + } + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 66fbc947..87d17118 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,11 @@ "types": "./dist/navigation/index.d.ts", "default": "./dist/navigation/index.js" }, + "./types": { + "source": "./src/types.ts", + "types": "./dist/types.d.ts", + "default": "./dist/types.js" + }, "./package.json": "./package.json" }, "scripts": { @@ -81,6 +86,7 @@ "@rock-js/plugin-brownfield-android": "^0.13.3", "@rock-js/plugin-brownfield-ios": "^0.13.3", "@rock-js/tools": "^0.13.3", + "ajv": "^6.14.0", "commander": "^14.0.3", "quicktype-core": "^23.2.6", "quicktype-typescript-input": "^23.2.6", diff --git a/packages/cli/schema.json b/packages/cli/schema.json new file mode 100644 index 00000000..93f25766 --- /dev/null +++ b/packages/cli/schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "React Native Brownfield CLI config", + "description": "Configuration for react-native-brownfield.config.json and package.json#react-native-brownfield.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for editor tooling" + }, + "archive": { + "type": "boolean", + "description": "Create an Xcode archive (IPA) of the build, required for uploading to App Store Connect or distributing to TestFlight." + }, + "buildFolder": { + "type": "string", + "description": "Location for iOS build artifacts. Corresponds to Xcode's \"-derivedDataPath\". By default, the '/.brownfield/build' path is used." + }, + "brownie": { + "type": "object", + "description": "Configuration for Brownie code generation. Use this nested object in react-native-brownfield config instead of the legacy package.json#brownie block.", + "additionalProperties": false, + "properties": { + "kotlin": { + "type": "string", + "description": "Directory where generated Kotlin Brownie store files should be written." + }, + "kotlinPackageName": { + "type": "string", + "description": "Kotlin package name used in generated Brownie store files." + } + } + }, + "configuration": { + "type": "string", + "description": "Explicitly set the scheme configuration to use. This option is case sensitive." + }, + "destination": { + "type": "array", + "description": "Define destination values for the build. You can pass multiple destinations as separate values. Supported values include \"simulator\", \"device\", or full xcodebuild destination strings.", + "items": { + "type": "string" + } + }, + "exportExtraParams": { + "type": "array", + "description": "Custom params passed to the xcodebuild export archive command.", + "items": { + "type": "string" + } + }, + "exportOptionsPlist": { + "type": "string", + "description": "Name of the export options file for archiving. Defaults to ExportOptions.plist." + }, + "extraParams": { + "type": "array", + "description": "Custom params passed to the xcodebuild command.", + "items": { + "type": "string" + } + }, + "installPods": { + "type": "boolean", + "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." + }, + "local": { + "type": "boolean", + "description": "Force a local build with xcodebuild." + }, + "moduleName": { + "type": "string", + "description": "AAR module name." + }, + "newArch": { + "type": "boolean", + "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." + }, + "usePrebuiltRnCore": { + "type": "boolean", + "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." + }, + "scheme": { + "type": "string", + "description": "Explicitly set the Xcode scheme to use." + }, + "target": { + "type": "string", + "description": "Explicitly set the Xcode target to use." + }, + "variant": { + "type": "string", + "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." + }, + "verbose": { + "type": "boolean", + "description": "Enable verbose logging." + } + } +} diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts new file mode 100644 index 00000000..651c1361 --- /dev/null +++ b/packages/cli/src/__tests__/config.test.ts @@ -0,0 +1,255 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import * as rockTools from '@rock-js/tools'; +import { Command } from 'commander'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@rock-js/tools', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logger: { + ...actual.logger, + warn: vi.fn(), + debug: vi.fn(), + setVerbose: vi.fn(), + }, + }; +}); + +vi.mock('../brownfield/utils/paths.js', () => ({ + findProjectRoot: vi.fn(() => process.cwd()), +})); + +import { + addBrownfieldConfig, + loadBrownfieldConfig, + validateBrownfieldCLIConfig, +} from '../config.js'; + +const mockLoggerWarn = rockTools.logger.warn as ReturnType; +const originalCwd = process.cwd(); + +function createTempProject({ + packageJsonConfig, + jsConfig, + jsonConfig, +}: { + packageJsonConfig?: Record; + jsConfig?: Record; + jsonConfig?: Record; +} = {}): string { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brownfield-config-')); + + const packageJson: Record = { + name: 'temp-project', + version: '1.0.0', + }; + + if (packageJsonConfig !== undefined) { + packageJson['react-native-brownfield'] = packageJsonConfig; + } + + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify(packageJson, null, 2) + ); + + if (jsConfig !== undefined) { + fs.writeFileSync( + path.join(tempDir, 'react-native-brownfield.config.js'), + `module.exports = ${JSON.stringify(jsConfig, null, 2)};\n` + ); + } + + if (jsonConfig !== undefined) { + fs.writeFileSync( + path.join(tempDir, 'react-native-brownfield.config.json'), + JSON.stringify(jsonConfig, null, 2) + ); + } + + return tempDir; +} + +function createCommand(): Command { + return new Command() + .option('--scheme ') + .option('--install-pods') + .option('--destination ') + .option('--target ') + .option('--extra-params '); +} + +describe('loadBrownfieldConfig', () => { + let tempDir: string | null = null; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + process.chdir(originalCwd); + + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it('loads config from package.json', () => { + tempDir = createTempProject({ + packageJsonConfig: { + scheme: 'PackageScheme', + destination: ['simulator'], + }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + scheme: 'PackageScheme', + destination: ['simulator'], + }); + }); + + it('loads config from a JavaScript config file', () => { + tempDir = createTempProject({ + jsConfig: { + scheme: 'JsScheme', + installPods: true, + }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + scheme: 'JsScheme', + installPods: true, + }); + }); + + it('loads config from a JSON config file', () => { + tempDir = createTempProject({ + jsonConfig: { + scheme: 'JsonScheme', + verbose: true, + }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + scheme: 'JsonScheme', + verbose: true, + }); + }); + + it('returns an empty config when no source exists', () => { + tempDir = createTempProject(); + + expect(loadBrownfieldConfig(tempDir)).toEqual({}); + }); + + it('throws when multiple config sources are present', () => { + tempDir = createTempProject({ + packageJsonConfig: { + scheme: 'PackageScheme', + }, + jsConfig: { + scheme: 'JsScheme', + }, + }); + + expect(() => loadBrownfieldConfig(tempDir!)).toThrow( + 'Project has multiple Brownfield configuration files' + ); + }); +}); + +describe('validateBrownfieldCLIConfig', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('does not warn for a schema-valid config', () => { + validateBrownfieldCLIConfig({ + scheme: 'AppScheme', + destination: ['simulator'], + usePrebuiltRnCore: true, + verbose: true, + brownie: { + kotlin: + './android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/', + kotlinPackageName: 'com.rnapp.brownfieldlib', + }, + }); + + expect(mockLoggerWarn).not.toHaveBeenCalled(); + }); + + it('warns for a schema-invalid config', () => { + validateBrownfieldCLIConfig({ + unsupportedOption: true, + }); + + expect(mockLoggerWarn).toHaveBeenCalledTimes(1); + expect(mockLoggerWarn.mock.calls[0]?.[0]).toContain( + 'Brownfield configuration has some issues:' + ); + }); +}); + +describe('addBrownfieldConfig', () => { + let tempDir: string | null = null; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + process.chdir(originalCwd); + + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it('applies config values to undefined CLI options', () => { + tempDir = createTempProject({ + packageJsonConfig: { + scheme: 'ConfigScheme', + installPods: true, + destination: ['simulator'], + }, + }); + process.chdir(tempDir); + + const command = createCommand(); + command.setOptionValue('target', 'MyApp'); + + addBrownfieldConfig(command); + + expect(command.optsWithGlobals()).toMatchObject({ + scheme: 'ConfigScheme', + installPods: true, + destination: ['simulator'], + target: 'MyApp', + }); + expect(mockLoggerWarn).not.toHaveBeenCalled(); + }); + + it('warns and preserves the CLI value when it overrides the config', () => { + tempDir = createTempProject({ + packageJsonConfig: { + scheme: 'ConfigScheme', + }, + }); + process.chdir(tempDir); + + const command = createCommand(); + command.setOptionValue('scheme', 'CliScheme'); + + addBrownfieldConfig(command); + + expect(command.optsWithGlobals().scheme).toBe('CliScheme'); + expect(mockLoggerWarn).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts index 64088ee2..52d6f622 100644 --- a/packages/cli/src/brownfield/commands/packageIos.ts +++ b/packages/cli/src/brownfield/commands/packageIos.ts @@ -4,7 +4,6 @@ import path from 'node:path'; import { getBuildOptions, mergeFrameworks, - type BuildFlags as AppleBuildFlags, } from '@rock-js/platform-apple-helpers'; import { packageIosAction } from '@rock-js/plugin-brownfield-ios'; import { @@ -28,6 +27,7 @@ import { import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js'; import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js'; import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js'; +import { PackageIosOptions } from '../../types.js'; /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */ const USE_PREBUILT_RN_CORE_HELP = @@ -54,11 +54,6 @@ export function parseUsePrebuiltRnCoreArgument( ); } -type PackageIosCliFlags = AppleBuildFlags & { - /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */ - usePrebuiltRnCore?: boolean; -}; - export const packageIosCommand = curryOptions( new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => @@ -78,7 +73,7 @@ export const packageIosCommand = curryOptions( .argParser(parseUsePrebuiltRnCoreArgument) ) .action( - actionRunner(async (options: PackageIosCliFlags) => { + actionRunner(async (options: PackageIosOptions) => { const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios'); const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot }); diff --git a/packages/cli/src/brownie/__tests__/commands/codegen.test.ts b/packages/cli/src/brownie/__tests__/commands/codegen.test.ts index deb5473d..76fb0b5d 100644 --- a/packages/cli/src/brownie/__tests__/commands/codegen.test.ts +++ b/packages/cli/src/brownie/__tests__/commands/codegen.test.ts @@ -113,6 +113,27 @@ describe('runCodegen', () => { expect(mockGenerateSwift).not.toHaveBeenCalled(); }); + it('throws when legacy and new brownie configs are both provided', async () => { + tempDir = createTempPackageJson({ + brownie: { + kotlin: './LegacyGenerated', + }, + }); + mockCwd.mockReturnValue(tempDir); + + await expect( + runCodegen({ + brownie: { + kotlin: './NewGenerated', + }, + }) + ).rejects.toThrow( + 'Cannot use both legacy and new Brownie configuration formats simultaneously.' + ); + + expect(mockDiscoverStores).not.toHaveBeenCalled(); + }); + it('generates swift and kotlin by default when kotlin is configured', async () => { tempDir = createTempPackageJson({ brownie: { diff --git a/packages/cli/src/brownie/commands/codegen.ts b/packages/cli/src/brownie/commands/codegen.ts index 28d2a5bc..83fb1fd3 100644 --- a/packages/cli/src/brownie/commands/codegen.ts +++ b/packages/cli/src/brownie/commands/codegen.ts @@ -7,6 +7,7 @@ import { intro, logger, outro } from '@rock-js/tools'; import { QuickTypeError } from 'quicktype-core'; import { actionRunner } from '../../shared/index.js'; import { + hasLegacyConfig, loadConfig, getSwiftOutputPath, type BrownieConfig, @@ -84,17 +85,34 @@ async function generateForStore( } } -export type RunCodegenOptions = { platform?: Platform }; +export type RunCodegenOptions = { + platform?: Platform; + brownie?: BrownieConfig; +}; /** * Runs the codegen command with the given arguments. */ -export async function runCodegen({ platform }: RunCodegenOptions) { +export async function runCodegen({ platform, brownie }: RunCodegenOptions) { intro( `Running Brownie codegen for ${platform ? `platform ${platform}` : 'all platforms'}` ); - const config = loadConfig(); + const legacyConfig = hasLegacyConfig() ? loadConfig() : undefined; + + if (legacyConfig && brownie) { + throw new Error( + 'Cannot use both legacy and new Brownie configuration formats simultaneously. Please migrate to the new configuration format and remove legacy configuration files: https://oss.callstack.com/react-native-brownfield/docs/api-reference/configuration#migrating-from-legacy-brownie-configuration' + ); + } + + if (legacyConfig) { + logger.warn( + 'You are using legacy Brownie configuration. Please migrate to the new configuration format. See the documentation for more details: https://oss.callstack.com/react-native-brownfield/docs/api-reference/configuration#migrating-from-legacy-brownie-configuration' + ); + } + + const config = brownie || legacyConfig || {}; if (platform && !['swift', 'kotlin'].includes(platform)) { logger.error(`Invalid platform: ${platform}. Must be 'swift' or 'kotlin'`); diff --git a/packages/cli/src/brownie/config.ts b/packages/cli/src/brownie/config.ts index 7b3d2192..2965e73c 100644 --- a/packages/cli/src/brownie/config.ts +++ b/packages/cli/src/brownie/config.ts @@ -11,6 +11,16 @@ interface PackageJson { brownie?: BrownieConfig; } +function loadPackageJson(projectRoot: string = process.cwd()): PackageJson { + const packageJsonPath = path.resolve(projectRoot, 'package.json'); + + if (!fs.existsSync(packageJsonPath)) { + throw new Error('package.json not found'); + } + + return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson; +} + /** * Checks if @callstack/brownie package is installed. */ @@ -54,18 +64,20 @@ export function getSwiftOutputPath( return path.join(browniePath, 'ios', 'Generated'); } +/** + * Returns whether package.json contains legacy brownie config. + */ +export function hasLegacyConfig(projectRoot: string = process.cwd()): boolean { + const packageJson = loadPackageJson(projectRoot); + + return Object.prototype.hasOwnProperty.call(packageJson, 'brownie'); +} + /** * Loads brownie config from package.json in the current working directory. */ export function loadConfig(): BrownieConfig { - const packageJsonPath = path.resolve(process.cwd(), 'package.json'); - - if (!fs.existsSync(packageJsonPath)) { - throw new Error('package.json not found'); - } + const packageJson = loadPackageJson(); - const packageJson: PackageJson = JSON.parse( - fs.readFileSync(packageJsonPath, 'utf-8') - ); return packageJson.brownie ?? {}; } diff --git a/packages/cli/src/brownie/index.ts b/packages/cli/src/brownie/index.ts index fd4b48c5..b3377616 100644 --- a/packages/cli/src/brownie/index.ts +++ b/packages/cli/src/brownie/index.ts @@ -8,4 +8,5 @@ export * from './store-discovery.js'; export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/brownie')}${styleText('whiteBright', ' - Shared state management CLI for React Native Brownfield')}`; export { Commands }; +export type { BrownieConfig } from './config.js'; export default Commands; diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 00000000..e32e503f --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,83 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import Ajv from 'ajv'; + +import type { BrownfieldConfig } from './types.js'; +import { findProjectRoot } from './brownfield/utils/paths.js'; + +import BrownfieldSchema from '../schema.json' with { type: 'json' }; +import { logger } from '@rock-js/tools'; +import { Command } from 'commander'; + +const JS_CONFIG_FILE_NAME = 'react-native-brownfield.config.js'; +const JSON_CONFIG_FILE_NAME = 'react-native-brownfield.config.json'; +const PACKAGE_JSON_CONFIG_KEY = 'react-native-brownfield'; + +const SEPARATOR = '\n● '; + +const ajv = new Ajv({ allErrors: true }); +const validateBrownfieldConfig = ajv.compile(BrownfieldSchema); + +export function validateBrownfieldCLIConfig(config: unknown): void { + if (!validateBrownfieldConfig(config)) { + logger.warn( + `Brownfield configuration has some issues: ${SEPARATOR}${ajv.errorsText(validateBrownfieldConfig.errors, { separator: SEPARATOR, dataVar: 'config' })}.` + ); + } +} + +export function loadBrownfieldConfig( + projectRoot: string = findProjectRoot() +): BrownfieldConfig { + const require = createRequire(path.join(projectRoot, 'package.json')); + + const jsConfigFilePath = path.join(projectRoot, JS_CONFIG_FILE_NAME); + const jsonConfigFilePath = path.join(projectRoot, JSON_CONFIG_FILE_NAME); + const packageJsonPath = path.join(projectRoot, 'package.json'); + const packageJson = require(packageJsonPath) as Record; + + if ( + [ + fs.existsSync(jsConfigFilePath), + fs.existsSync(jsonConfigFilePath), + packageJson[PACKAGE_JSON_CONFIG_KEY], + ].filter(Boolean).length > 1 + ) { + throw new Error('Project has multiple Brownfield configuration files'); + } + + if (fs.existsSync(jsConfigFilePath)) { + return require(jsConfigFilePath) as BrownfieldConfig; + } + + if (fs.existsSync(jsonConfigFilePath)) { + return require(jsonConfigFilePath) as BrownfieldConfig; + } + + return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; +} + +export function addBrownfieldConfig(...args: any[]): void { + // Last argument is the current command instance + const command = args.at(-1) as Command; + + const reactNativeBrownfieldConfig = loadBrownfieldConfig(); + + validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); + + for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { + const cliOptionValue = command.optsWithGlobals()[key]; + + if (cliOptionValue !== undefined) { + logger.warn( + 'CLI option "%s" is overriding the react-native-brownfield config value.', + key + ); + continue; + } + + command.setOptionValue(key, value); + } +} diff --git a/packages/cli/src/navigation/commands/codegen.ts b/packages/cli/src/navigation/commands/codegen.ts index ce0c935a..8a4b988e 100644 --- a/packages/cli/src/navigation/commands/codegen.ts +++ b/packages/cli/src/navigation/commands/codegen.ts @@ -42,9 +42,7 @@ export const navigationCodegenCommand = new Command('navigation:codegen') const specPath = typeof args[0] === 'string' ? args[0] : undefined; const options = args.find( - ( - arg - ): arg is RunNavigationCodegenCommandOptions => + (arg): arg is RunNavigationCodegenCommandOptions => typeof arg === 'object' && arg !== null && 'dryRun' in arg ) ?? {}; diff --git a/packages/cli/src/shared/utils/__tests__/cli.test.ts b/packages/cli/src/shared/utils/__tests__/cli.test.ts index b9d91cd3..64c85ef2 100644 --- a/packages/cli/src/shared/utils/__tests__/cli.test.ts +++ b/packages/cli/src/shared/utils/__tests__/cli.test.ts @@ -1,9 +1,14 @@ import * as rockTools from '@rock-js/tools'; +import * as configModule from '../../../config.js'; -import { expect, Mock, test, vi } from 'vitest'; +import { beforeEach, expect, Mock, test, vi } from 'vitest'; import { actionRunner } from '../cli.js'; +vi.mock('../../../config.js', () => ({ + addBrownfieldConfig: vi.fn(), +})); + vi.mock('@rock-js/tools', async (importOriginal) => { const actual = await importOriginal(); return { @@ -23,6 +28,7 @@ const processExitMock = vi.spyOn(process, 'exit').mockImplementation(() => { // no-op }); +const mockAddBrownfieldConfig = configModule.addBrownfieldConfig as Mock; const mockLoggerError = rockTools.logger.error as Mock; const FAILING_ACTION_ERROR_MESSAGE = 'Test error'; @@ -32,6 +38,10 @@ const createWrappedFailingAction = (ErrorCls: new (message: string) => Error) => throw new ErrorCls(FAILING_ACTION_ERROR_MESSAGE); }); +beforeEach(() => { + vi.clearAllMocks(); +}); + test('actionRunner should call the wrapped function', async () => { const mockAction = vi.fn(async () => Promise.resolve()); const wrappedAction = actionRunner(mockAction); @@ -41,6 +51,15 @@ test('actionRunner should call the wrapped function', async () => { expect(mockAction).toHaveBeenCalledOnce(); }); +test('actionRunner should call addBrownfieldConfig with wrapped args', async () => { + const mockAction = vi.fn(async (_a: number, _b: number) => Promise.resolve()); + const wrappedAction = actionRunner(mockAction); + + await wrappedAction(1, 2); + + expect(mockAddBrownfieldConfig).toHaveBeenCalledExactlyOnceWith(1, 2); +}); + test('actionRunner should gracefully handle Errors', async () => { const wrappedActionExpectation = expect( createWrappedFailingAction(Error)(1, 2) diff --git a/packages/cli/src/shared/utils/cli.ts b/packages/cli/src/shared/utils/cli.ts index aa1b2975..6b81992d 100644 --- a/packages/cli/src/shared/utils/cli.ts +++ b/packages/cli/src/shared/utils/cli.ts @@ -1,6 +1,7 @@ import { logger, RockError, type RockCLIOptions } from '@rock-js/tools'; import type { Command } from 'commander'; +import { addBrownfieldConfig } from '../../config.js'; export function curryOptions(programCommand: Command, options: RockCLIOptions) { options.forEach((option) => { @@ -26,6 +27,7 @@ export function curryOptions(programCommand: Command, options: RockCLIOptions) { export function actionRunner(fn: (...args: T[]) => Promise) { return async function wrappedCLIAction(...args: T[]) { try { + addBrownfieldConfig(...args); await fn(...args); } catch (error) { if (error instanceof RockError) { diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts new file mode 100644 index 00000000..9307c52e --- /dev/null +++ b/packages/cli/src/types.ts @@ -0,0 +1,37 @@ +import { type PackageAarFlags } from '@rock-js/platform-android'; + +import { type PublishLocalAarFlags } from '@rock-js/platform-android'; +import { type BuildFlags as AppleBuildFlags } from '@rock-js/platform-apple-helpers'; + +export type BrownfieldCommonOptions = Partial<{ + verbose: boolean; +}>; + +export type BrownfieldConfigMetadata = Partial<{ + $schema: string; +}>; + +export type BrownieConfig = { + kotlin?: string; + kotlinPackageName?: string; +}; + +export type PackageIosOptions = AppleBuildFlags & { + usePrebuiltRnCore?: boolean; +}; + +export type BrownfieldPackageAndroidOptions = BrownfieldCommonOptions & + Partial; +export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & + Partial; +export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & + Partial; + +export type BrownfieldAndroidConfig = Partial & + Partial; +export type BrownfieldIosConfig = Partial; + +export type BrownfieldConfig = BrownfieldConfigMetadata & + BrownfieldCommonOptions & + BrownfieldAndroidConfig & + BrownfieldIosConfig & { brownie?: BrownieConfig }; diff --git a/packages/react-native-brownfield/src/index.ts b/packages/react-native-brownfield/src/index.ts index 14024084..457f69b3 100644 --- a/packages/react-native-brownfield/src/index.ts +++ b/packages/react-native-brownfield/src/index.ts @@ -2,6 +2,8 @@ import { Platform } from 'react-native'; import ReactNativeBrownfieldModule from './NativeReactNativeBrownfieldModule'; +export type { BrownfieldConfig } from '@callstack/brownfield-cli/types'; + export interface MessageEvent { data: unknown; } diff --git a/yarn.lock b/yarn.lock index 1524200e..fab24487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1688,6 +1688,7 @@ __metadata: "@types/babel__preset-env": "npm:^7.10.0" "@types/node": "npm:^25.5.0" "@vitest/coverage-v8": "npm:^4.1.0" + ajv: "npm:^6.14.0" commander: "npm:^14.0.3" eslint: "npm:^9.39.3" globals: "npm:^17.3.0"