From 7e7602c63c8c5c21eff76e9a98f1d6629bafc0ce Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 18 May 2026 12:59:08 +0200 Subject: [PATCH 01/11] feat: brownfield config file --- apps/RNApp/react-native-brownfield.config.js | 6 ++ packages/cli/src/brownfield/config.ts | 98 ++++++++++++++++++++ packages/cli/src/brownfield/index.ts | 3 + packages/cli/src/brownfield/schema.json | 85 +++++++++++++++++ packages/cli/src/brownfield/types.ts | 23 +++++ packages/cli/src/index.ts | 42 ++++++++- 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 apps/RNApp/react-native-brownfield.config.js create mode 100644 packages/cli/src/brownfield/config.ts create mode 100644 packages/cli/src/brownfield/schema.json create mode 100644 packages/cli/src/brownfield/types.ts diff --git a/apps/RNApp/react-native-brownfield.config.js b/apps/RNApp/react-native-brownfield.config.js new file mode 100644 index 00000000..eac6f863 --- /dev/null +++ b/apps/RNApp/react-native-brownfield.config.js @@ -0,0 +1,6 @@ +/** + * @typedef {import('@callstack/').BrownfieldConfig} BrownfieldConfig + */ +module.exports = { + moduleName: 'SSS', +}; diff --git a/packages/cli/src/brownfield/config.ts b/packages/cli/src/brownfield/config.ts new file mode 100644 index 00000000..e60e8b84 --- /dev/null +++ b/packages/cli/src/brownfield/config.ts @@ -0,0 +1,98 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import type { BrownfieldConfig } from './types.js'; +import { findProjectRoot } from './utils/paths.js'; + +const CONFIG_FILE_NAMES = [ + 'react-native-brownfield.config.js', + 'react-native-brownfield.config.json', +] as const; + +const PACKAGE_JSON_CONFIG_KEY = 'react-native-brownfield'; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validateConfig(value: unknown, source: string): BrownfieldConfig { + if (!isRecord(value)) { + throw new Error( + `Brownfield config in ${source} must export an object.` + ); + } + + return value as BrownfieldConfig; +} + +function normalizeModuleValue( + moduleValue: unknown, + source: string +): BrownfieldConfig { + if ( + isRecord(moduleValue) && + 'default' in moduleValue && + moduleValue.default !== undefined + ) { + return validateConfig(moduleValue.default, source); + } + + return validateConfig(moduleValue, source); +} + +function loadModuleFromFile( + require: ReturnType, + filePath: string +) { + const resolvedPath = require.resolve(filePath); + delete require.cache[resolvedPath]; + return require(resolvedPath); +} + +function loadConfigFromFile( + require: ReturnType, + filePath: string +): BrownfieldConfig { + return normalizeModuleValue( + loadModuleFromFile(require, filePath), + path.basename(filePath) + ); +} + +/** + * Loads Brownfield CLI config from project root. + * Search order: + * 1. react-native-brownfield.config.js + * 2. react-native-brownfield.config.json + * 3. package.json#react-native-brownfield + */ +export function loadConfig( + projectRoot: string = findProjectRoot() +): BrownfieldConfig { + const require = createRequire(path.join(projectRoot, 'package.json')); + + for (const fileName of CONFIG_FILE_NAMES) { + const filePath = path.join(projectRoot, fileName); + if (fs.existsSync(filePath)) { + return loadConfigFromFile(require, filePath); + } + } + + const packageJsonPath = path.join(projectRoot, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + return {}; + } + + const packageJson = loadModuleFromFile(require, packageJsonPath) as Record< + string, + unknown + >; + + const packageJsonConfig = packageJson[PACKAGE_JSON_CONFIG_KEY]; + if (packageJsonConfig === undefined) { + return {}; + } + + return validateConfig(packageJsonConfig, 'package.json'); +} \ No newline at end of file diff --git a/packages/cli/src/brownfield/index.ts b/packages/cli/src/brownfield/index.ts index 55606d3f..9f9d9297 100644 --- a/packages/cli/src/brownfield/index.ts +++ b/packages/cli/src/brownfield/index.ts @@ -9,6 +9,9 @@ import { } from './commands/publishAndroid.js'; import { packageIosCommand, packageIosExample } from './commands/packageIos.js'; +export type * from './types.js'; +export * from './config.js'; + export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/react-native-brownfield')}${styleText('whiteBright', ' - utilities for React Native Brownfield projects')}`; export const Commands = { diff --git a/packages/cli/src/brownfield/schema.json b/packages/cli/src/brownfield/schema.json new file mode 100644 index 00000000..9cb64fa8 --- /dev/null +++ b/packages/cli/src/brownfield/schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://unpkg.com/@callstack/brownfield-cli/brownfield.schema.json", + "title": "React Native Brownfield CLI config", + "description": "Configuration for the Brownfield packaging commands.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for editor tooling." + }, + "verbose": { + "type": "boolean", + "description": "Enable verbose logging." + }, + "variant": { + "type": "string", + "description": "Android build variant, for example debug or freeRelease.", + "default": "debug" + }, + "moduleName": { + "type": "string", + "description": "Android AAR module name." + }, + "configuration": { + "type": "string", + "description": "Explicit iOS scheme configuration to use. This value is case sensitive." + }, + "scheme": { + "type": "string", + "description": "Explicit iOS Xcode scheme to use." + }, + "target": { + "type": "string", + "description": "Explicit iOS Xcode target to use." + }, + "extraParams": { + "type": "array", + "description": "Custom parameters passed to xcodebuild.", + "items": { + "type": "string" + } + }, + "exportExtraParams": { + "type": "array", + "description": "Custom parameters passed to xcodebuild during archive export.", + "items": { + "type": "string" + } + }, + "exportOptionsPlist": { + "type": "string", + "description": "Export options plist file name used for archiving.", + "default": "ExportOptions.plist" + }, + "buildFolder": { + "type": "string", + "description": "Location for iOS build artifacts." + }, + "destination": { + "type": "array", + "description": "iOS build destinations, such as simulator, device, or custom xcodebuild destination strings.", + "items": { + "type": "string" + } + }, + "archive": { + "type": "boolean", + "description": "Create an Xcode archive for the iOS build." + }, + "installPods": { + "type": "boolean", + "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." + }, + "newArch": { + "type": "boolean", + "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." + }, + "local": { + "type": "boolean", + "description": "Force a local iOS build with xcodebuild." + } + } +} diff --git a/packages/cli/src/brownfield/types.ts b/packages/cli/src/brownfield/types.ts new file mode 100644 index 00000000..bd9bd7e0 --- /dev/null +++ b/packages/cli/src/brownfield/types.ts @@ -0,0 +1,23 @@ +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 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 = BrownfieldAndroidConfig & BrownfieldIosConfig diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 867a3dbb..197e4091 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,11 +2,13 @@ import { styleText } from 'node:util'; import { logger } from '@rock-js/tools'; -import { Command } from 'commander'; +import { Command, type Option } from 'commander'; import { ExampleUsage } from './shared/index.js'; import brownfieldCommands, { groupName as brownfieldCommandsGroupName, + loadConfig, + type BrownfieldConfig, } from './brownfield/index.js'; import brownieCommands, { groupName as brownieCommandsGroupName, @@ -43,6 +45,38 @@ program.configureHelp({ styleSubcommandText: (str) => styleText('blue', str), }); +function getCommandOptions(command: Command): Option[] { + return (command as Command & { options: Option[] }).options; +} + +function applyConfigValueToCommand(command: Command, key: string, value: unknown) { + const option = getCommandOptions(command).find( + (candidate) => candidate.attributeName() === key + ); + + if (!option) { + return; + } + + command.setOptionValueWithSource(key, value, 'config'); +} + +function applyBrownfieldConfigToCommands(config: BrownfieldConfig) { + for (const [key, value] of Object.entries(config)) { + if (value === undefined) { + continue; + } + + applyConfigValueToCommand(program, key, value); + + for (const command of Object.values(brownfieldCommands)) { + if (command instanceof Command) { + applyConfigValueToCommand(command, key, value); + } + } + } +} + function registrationHelper( commandsRegistration: Record, groupName: string @@ -73,6 +107,12 @@ function registrationHelper( } } +const reactNativeBrownfieldConfig = loadConfig() + +console.log('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); + +applyBrownfieldConfigToCommands(reactNativeBrownfieldConfig); + registrationHelper(brownfieldCommands, brownfieldCommandsGroupName); registrationHelper(brownieCommands, brownieCommandsGroupName); registrationHelper(navigationCommands, navigationCommandsGroupName); From 2e498231d1cd910355cbed8c28072a86b02fa049 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 18 May 2026 13:20:17 +0200 Subject: [PATCH 02/11] fix: rewire config types --- apps/RNApp/react-native-brownfield.config.js | 5 +++-- packages/react-native-brownfield/src/index.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/RNApp/react-native-brownfield.config.js b/apps/RNApp/react-native-brownfield.config.js index eac6f863..c9f0a98f 100644 --- a/apps/RNApp/react-native-brownfield.config.js +++ b/apps/RNApp/react-native-brownfield.config.js @@ -1,6 +1,7 @@ /** - * @typedef {import('@callstack/').BrownfieldConfig} BrownfieldConfig + * @type {import('@callstack/react-native-brownfield').BrownfieldConfig} */ module.exports = { - moduleName: 'SSS', + moduleName: ':BrownfieldLib', + scheme: 'BrownfieldLib', }; diff --git a/packages/react-native-brownfield/src/index.ts b/packages/react-native-brownfield/src/index.ts index 14024084..225a7aab 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/brownfield'; + export interface MessageEvent { data: unknown; } From de2fc0408f5fd1653a14ff83fe142eb7723725fb Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 18 May 2026 14:55:32 +0200 Subject: [PATCH 03/11] feat: schema validation --- apps/RNApp/package.json | 6 +- packages/cli/package.json | 1 + packages/cli/schema.json | 70 ++++++++++++++++++++ packages/cli/src/brownfield/config.ts | 83 ++++++------------------ packages/cli/src/brownfield/schema.json | 85 ------------------------- packages/cli/src/brownfield/types.ts | 6 +- packages/cli/src/index.ts | 19 ++---- yarn.lock | 1 + 8 files changed, 106 insertions(+), 165 deletions(-) create mode 100644 packages/cli/schema.json delete mode 100644 packages/cli/src/brownfield/schema.json diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json index 8ec3f40e..20d8464c 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", - "brownfield:publish:android": "brownfield publish:android --module-name :BrownfieldLib", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release", + "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", diff --git a/packages/cli/package.json b/packages/cli/package.json index 141fd1bc..a9998814 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -72,6 +72,7 @@ "@react-native-community/cli-config-android": "*" }, "dependencies": { + "ajv": "^6.14.0", "@expo/config": "^12.0.13", "@react-native-community/cli-config": "^20.0.0", "@react-native-community/cli-config-android": "^20.0.0", diff --git a/packages/cli/schema.json b/packages/cli/schema.json new file mode 100644 index 00000000..e601cf42 --- /dev/null +++ b/packages/cli/schema.json @@ -0,0 +1,70 @@ +{ + "$ref": "#/definitions/BrownfieldConfig", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BrownfieldConfig": { + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "archive": { + "type": "boolean" + }, + "buildFolder": { + "type": "string" + }, + "configuration": { + "type": "string" + }, + "destination": { + "items": { + "type": "string" + }, + "type": "array" + }, + "exportExtraParams": { + "items": { + "type": "string" + }, + "type": "array" + }, + "exportOptionsPlist": { + "type": "string" + }, + "extraParams": { + "items": { + "type": "string" + }, + "type": "array" + }, + "installPods": { + "type": "boolean" + }, + "local": { + "type": "boolean" + }, + "moduleName": { + "type": "string" + }, + "newArch": { + "type": "boolean" + }, + "scheme": { + "type": "string" + }, + "target": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "verbose": { + "type": "boolean" + } + }, + "type": "object" + } + } +} + diff --git a/packages/cli/src/brownfield/config.ts b/packages/cli/src/brownfield/config.ts index e60e8b84..52224b62 100644 --- a/packages/cli/src/brownfield/config.ts +++ b/packages/cli/src/brownfield/config.ts @@ -2,62 +2,27 @@ 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 './utils/paths.js'; -const CONFIG_FILE_NAMES = [ - 'react-native-brownfield.config.js', - 'react-native-brownfield.config.json', -] as const; +import BrownfieldSchema from '../../schema.json' with { type: 'json' }; +import { logger } from '@rock-js/tools'; +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'; -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function validateConfig(value: unknown, source: string): BrownfieldConfig { - if (!isRecord(value)) { - throw new Error( - `Brownfield config in ${source} must export an object.` - ); - } +const SEPARATOR = '\n● '; - return value as BrownfieldConfig; -} +const ajv = new Ajv({ allErrors: true }); +const validateBrownfieldConfig = ajv.compile(BrownfieldSchema); -function normalizeModuleValue( - moduleValue: unknown, - source: string -): BrownfieldConfig { - if ( - isRecord(moduleValue) && - 'default' in moduleValue && - moduleValue.default !== undefined - ) { - return validateConfig(moduleValue.default, source); +export function validateConfig(config: unknown) { + if (!validateBrownfieldConfig(config)) { + logger.warn(`Brownfield configuration has some issues: ${SEPARATOR}${ajv.errorsText(validateBrownfieldConfig.errors, { separator: SEPARATOR, dataVar: 'config' })}.`); } - - return validateConfig(moduleValue, source); -} - -function loadModuleFromFile( - require: ReturnType, - filePath: string -) { - const resolvedPath = require.resolve(filePath); - delete require.cache[resolvedPath]; - return require(resolvedPath); -} - -function loadConfigFromFile( - require: ReturnType, - filePath: string -): BrownfieldConfig { - return normalizeModuleValue( - loadModuleFromFile(require, filePath), - path.basename(filePath) - ); } /** @@ -72,27 +37,21 @@ export function loadConfig( ): BrownfieldConfig { const require = createRequire(path.join(projectRoot, 'package.json')); - for (const fileName of CONFIG_FILE_NAMES) { - const filePath = path.join(projectRoot, fileName); - if (fs.existsSync(filePath)) { - return loadConfigFromFile(require, filePath); - } + const jsConfigFilePath = path.join(projectRoot, JS_CONFIG_FILE_NAME); + if (fs.existsSync(jsConfigFilePath)) { + return require(jsConfigFilePath) as BrownfieldConfig; } - const packageJsonPath = path.join(projectRoot, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - return {}; + const jsonConfigFilePath = path.join(projectRoot, JSON_CONFIG_FILE_NAME); + if (fs.existsSync(jsonConfigFilePath)) { + return require(jsonConfigFilePath) as BrownfieldConfig; } - const packageJson = loadModuleFromFile(require, packageJsonPath) as Record< + const packageJsonPath = path.join(projectRoot, 'package.json'); + const packageJson = require(packageJsonPath) as Record< string, unknown >; - const packageJsonConfig = packageJson[PACKAGE_JSON_CONFIG_KEY]; - if (packageJsonConfig === undefined) { - return {}; - } - - return validateConfig(packageJsonConfig, 'package.json'); + return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; } \ No newline at end of file diff --git a/packages/cli/src/brownfield/schema.json b/packages/cli/src/brownfield/schema.json deleted file mode 100644 index 9cb64fa8..00000000 --- a/packages/cli/src/brownfield/schema.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://unpkg.com/@callstack/brownfield-cli/brownfield.schema.json", - "title": "React Native Brownfield CLI config", - "description": "Configuration for the Brownfield packaging commands.", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema reference for editor tooling." - }, - "verbose": { - "type": "boolean", - "description": "Enable verbose logging." - }, - "variant": { - "type": "string", - "description": "Android build variant, for example debug or freeRelease.", - "default": "debug" - }, - "moduleName": { - "type": "string", - "description": "Android AAR module name." - }, - "configuration": { - "type": "string", - "description": "Explicit iOS scheme configuration to use. This value is case sensitive." - }, - "scheme": { - "type": "string", - "description": "Explicit iOS Xcode scheme to use." - }, - "target": { - "type": "string", - "description": "Explicit iOS Xcode target to use." - }, - "extraParams": { - "type": "array", - "description": "Custom parameters passed to xcodebuild.", - "items": { - "type": "string" - } - }, - "exportExtraParams": { - "type": "array", - "description": "Custom parameters passed to xcodebuild during archive export.", - "items": { - "type": "string" - } - }, - "exportOptionsPlist": { - "type": "string", - "description": "Export options plist file name used for archiving.", - "default": "ExportOptions.plist" - }, - "buildFolder": { - "type": "string", - "description": "Location for iOS build artifacts." - }, - "destination": { - "type": "array", - "description": "iOS build destinations, such as simulator, device, or custom xcodebuild destination strings.", - "items": { - "type": "string" - } - }, - "archive": { - "type": "boolean", - "description": "Create an Xcode archive for the iOS build." - }, - "installPods": { - "type": "boolean", - "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." - }, - "newArch": { - "type": "boolean", - "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." - }, - "local": { - "type": "boolean", - "description": "Force a local iOS build with xcodebuild." - } - } -} diff --git a/packages/cli/src/brownfield/types.ts b/packages/cli/src/brownfield/types.ts index bd9bd7e0..5cc7553b 100644 --- a/packages/cli/src/brownfield/types.ts +++ b/packages/cli/src/brownfield/types.ts @@ -13,6 +13,10 @@ export type BrownfieldCommonOptions = Partial<{ verbose: boolean; }> +export type BrownfieldConfigMetadata = Partial<{ + $schema: string; +}> + export type BrownfieldPackageAndroidOptions = BrownfieldCommonOptions & Partial export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & Partial export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial @@ -20,4 +24,4 @@ export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial & Partial export type BrownfieldIosConfig = Partial -export type BrownfieldConfig = BrownfieldAndroidConfig & BrownfieldIosConfig +export type BrownfieldConfig = BrownfieldConfigMetadata & BrownfieldCommonOptions & BrownfieldAndroidConfig & BrownfieldIosConfig diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 197e4091..e29ab843 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,12 +2,13 @@ import { styleText } from 'node:util'; import { logger } from '@rock-js/tools'; -import { Command, type Option } from 'commander'; +import { Command } from 'commander'; import { ExampleUsage } from './shared/index.js'; import brownfieldCommands, { groupName as brownfieldCommandsGroupName, loadConfig, + validateConfig, type BrownfieldConfig, } from './brownfield/index.js'; import brownieCommands, { @@ -45,19 +46,7 @@ program.configureHelp({ styleSubcommandText: (str) => styleText('blue', str), }); -function getCommandOptions(command: Command): Option[] { - return (command as Command & { options: Option[] }).options; -} - function applyConfigValueToCommand(command: Command, key: string, value: unknown) { - const option = getCommandOptions(command).find( - (candidate) => candidate.attributeName() === key - ); - - if (!option) { - return; - } - command.setOptionValueWithSource(key, value, 'config'); } @@ -109,7 +98,9 @@ function registrationHelper( const reactNativeBrownfieldConfig = loadConfig() -console.log('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); +validateConfig(reactNativeBrownfieldConfig); + +console.debug('Loaded Brownfield config:', reactNativeBrownfieldConfig); applyBrownfieldConfigToCommands(reactNativeBrownfieldConfig); diff --git a/yarn.lock b/yarn.lock index fb84082d..9934a05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,6 +1766,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" From a54bf4afcef4e934a08fa9ec491965b02ef5597f Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 18 May 2026 15:09:19 +0200 Subject: [PATCH 04/11] fix: apply and log configuration --- packages/cli/src/index.ts | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e29ab843..425df29a 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,7 +9,6 @@ import brownfieldCommands, { groupName as brownfieldCommandsGroupName, loadConfig, validateConfig, - type BrownfieldConfig, } from './brownfield/index.js'; import brownieCommands, { groupName as brownieCommandsGroupName, @@ -46,23 +45,15 @@ program.configureHelp({ styleSubcommandText: (str) => styleText('blue', str), }); -function applyConfigValueToCommand(command: Command, key: string, value: unknown) { - command.setOptionValueWithSource(key, value, 'config'); -} +function applyBrownfieldCLIConfig() { + const reactNativeBrownfieldConfig = loadConfig() -function applyBrownfieldConfigToCommands(config: BrownfieldConfig) { - for (const [key, value] of Object.entries(config)) { - if (value === undefined) { - continue; - } + logger.debug('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); - applyConfigValueToCommand(program, key, value); + validateConfig(reactNativeBrownfieldConfig); - for (const command of Object.values(brownfieldCommands)) { - if (command instanceof Command) { - applyConfigValueToCommand(command, key, value); - } - } + for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { + program.setOptionValueWithSource(key, value, 'config'); } } @@ -96,14 +87,6 @@ function registrationHelper( } } -const reactNativeBrownfieldConfig = loadConfig() - -validateConfig(reactNativeBrownfieldConfig); - -console.debug('Loaded Brownfield config:', reactNativeBrownfieldConfig); - -applyBrownfieldConfigToCommands(reactNativeBrownfieldConfig); - registrationHelper(brownfieldCommands, brownfieldCommandsGroupName); registrationHelper(brownieCommands, brownieCommandsGroupName); registrationHelper(navigationCommands, navigationCommandsGroupName); @@ -113,6 +96,8 @@ program.commandsGroup('Utility commands').helpCommand('help [command]'); export function runCLI(argv: string[]): void { program.parse(argv); + applyBrownfieldCLIConfig() + if (!argv.slice(2).length) { program.outputHelp(); } From 64fb67d71ffe6c61842137c5b566418d391e4351 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Tue, 19 May 2026 11:10:25 +0200 Subject: [PATCH 05/11] chore: move files and export types --- packages/cli/package.json | 7 ++++- packages/cli/src/brownfield/index.ts | 3 -- packages/cli/src/{brownfield => }/config.ts | 29 ++++++++++++------- packages/cli/src/index.ts | 17 ++--------- packages/cli/src/{brownfield => }/types.ts | 0 packages/react-native-brownfield/src/index.ts | 2 +- 6 files changed, 27 insertions(+), 31 deletions(-) rename packages/cli/src/{brownfield => }/config.ts (69%) rename packages/cli/src/{brownfield => }/types.ts (100%) diff --git a/packages/cli/package.json b/packages/cli/package.json index a9998814..1704f2b1 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": { @@ -72,7 +77,6 @@ "@react-native-community/cli-config-android": "*" }, "dependencies": { - "ajv": "^6.14.0", "@expo/config": "^12.0.13", "@react-native-community/cli-config": "^20.0.0", "@react-native-community/cli-config-android": "^20.0.0", @@ -81,6 +85,7 @@ "@rock-js/plugin-brownfield-android": "^0.12.12", "@rock-js/plugin-brownfield-ios": "^0.12.12", "@rock-js/tools": "^0.12.12", + "ajv": "^6.14.0", "commander": "^14.0.3", "quicktype-core": "^23.2.6", "quicktype-typescript-input": "^23.2.6", diff --git a/packages/cli/src/brownfield/index.ts b/packages/cli/src/brownfield/index.ts index 9f9d9297..55606d3f 100644 --- a/packages/cli/src/brownfield/index.ts +++ b/packages/cli/src/brownfield/index.ts @@ -9,9 +9,6 @@ import { } from './commands/publishAndroid.js'; import { packageIosCommand, packageIosExample } from './commands/packageIos.js'; -export type * from './types.js'; -export * from './config.js'; - export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/react-native-brownfield')}${styleText('whiteBright', ' - utilities for React Native Brownfield projects')}`; export const Commands = { diff --git a/packages/cli/src/brownfield/config.ts b/packages/cli/src/config.ts similarity index 69% rename from packages/cli/src/brownfield/config.ts rename to packages/cli/src/config.ts index 52224b62..4cbb2aca 100644 --- a/packages/cli/src/brownfield/config.ts +++ b/packages/cli/src/config.ts @@ -5,10 +5,11 @@ import path from 'node:path'; import Ajv from 'ajv'; import type { BrownfieldConfig } from './types.js'; -import { findProjectRoot } from './utils/paths.js'; +import { findProjectRoot } from './brownfield/utils/paths.js'; -import BrownfieldSchema from '../../schema.json' with { type: 'json' }; +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'; @@ -19,20 +20,13 @@ const SEPARATOR = '\n● '; const ajv = new Ajv({ allErrors: true }); const validateBrownfieldConfig = ajv.compile(BrownfieldSchema); -export function validateConfig(config: unknown) { +function validateConfig(config: unknown) { if (!validateBrownfieldConfig(config)) { logger.warn(`Brownfield configuration has some issues: ${SEPARATOR}${ajv.errorsText(validateBrownfieldConfig.errors, { separator: SEPARATOR, dataVar: 'config' })}.`); } } -/** - * Loads Brownfield CLI config from project root. - * Search order: - * 1. react-native-brownfield.config.js - * 2. react-native-brownfield.config.json - * 3. package.json#react-native-brownfield - */ -export function loadConfig( +function loadBrownfieldConfig( projectRoot: string = findProjectRoot() ): BrownfieldConfig { const require = createRequire(path.join(projectRoot, 'package.json')); @@ -54,4 +48,17 @@ export function loadConfig( >; return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; +} + + +export function loadAndApplyBrownfieldCLIConfig(program: Command) { + const reactNativeBrownfieldConfig = loadBrownfieldConfig() + + logger.debug('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); + + validateConfig(reactNativeBrownfieldConfig); + + for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { + program.setOptionValueWithSource(key, value, 'config'); + } } \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 425df29a..682a48b3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,8 +7,6 @@ import { Command } from 'commander'; import { ExampleUsage } from './shared/index.js'; import brownfieldCommands, { groupName as brownfieldCommandsGroupName, - loadConfig, - validateConfig, } from './brownfield/index.js'; import brownieCommands, { groupName as brownieCommandsGroupName, @@ -16,6 +14,7 @@ import brownieCommands, { import navigationCommands, { groupName as navigationCommandsGroupName, } from './navigation/index.js'; +import { loadAndApplyBrownfieldCLIConfig } from './config.js'; const program = new Command(); @@ -45,18 +44,6 @@ program.configureHelp({ styleSubcommandText: (str) => styleText('blue', str), }); -function applyBrownfieldCLIConfig() { - const reactNativeBrownfieldConfig = loadConfig() - - logger.debug('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); - - validateConfig(reactNativeBrownfieldConfig); - - for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { - program.setOptionValueWithSource(key, value, 'config'); - } -} - function registrationHelper( commandsRegistration: Record, groupName: string @@ -96,7 +83,7 @@ program.commandsGroup('Utility commands').helpCommand('help [command]'); export function runCLI(argv: string[]): void { program.parse(argv); - applyBrownfieldCLIConfig() + loadAndApplyBrownfieldCLIConfig(program); if (!argv.slice(2).length) { program.outputHelp(); diff --git a/packages/cli/src/brownfield/types.ts b/packages/cli/src/types.ts similarity index 100% rename from packages/cli/src/brownfield/types.ts rename to packages/cli/src/types.ts diff --git a/packages/react-native-brownfield/src/index.ts b/packages/react-native-brownfield/src/index.ts index 225a7aab..457f69b3 100644 --- a/packages/react-native-brownfield/src/index.ts +++ b/packages/react-native-brownfield/src/index.ts @@ -2,7 +2,7 @@ import { Platform } from 'react-native'; import ReactNativeBrownfieldModule from './NativeReactNativeBrownfieldModule'; -export type { BrownfieldConfig } from '@callstack/brownfield-cli/brownfield'; +export type { BrownfieldConfig } from '@callstack/brownfield-cli/types'; export interface MessageEvent { data: unknown; From 04a9b05d8e0722d805907e7d7f1e1015b50037e3 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Tue, 19 May 2026 11:35:30 +0200 Subject: [PATCH 06/11] chore: add unit tests and export schema.json --- packages/cli/package.json | 3 +- packages/cli/src/__tests__/config.test.ts | 190 ++++++++++++++++++++++ packages/cli/src/config.ts | 26 ++- 3 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/__tests__/config.test.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 1704f2b1..756eeca0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -67,7 +67,8 @@ "!**/__fixtures__", "!**/__mocks__", "!**/.*", - "README.md" + "README.md", + "schema.json" ], "publishConfig": { "access": "public" diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts new file mode 100644 index 00000000..4c54f049 --- /dev/null +++ b/packages/cli/src/__tests__/config.test.ts @@ -0,0 +1,190 @@ +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, describe, expect, it, Mock, vi } from 'vitest'; + +import { + applyBrownfieldCLIConfig, + loadAndApplyBrownfieldCLIConfig, + loadBrownfieldConfig, + validateBrownfieldCLIConfig, +} from '../config.js'; + +vi.mock('@rock-js/tools', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + logger: { + ...actual.logger, + debug: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +const mockLoggerWarn = rockTools.logger.warn as Mock; + +function createTempProject(options?: { + packageJsonConfig?: Record; + jsConfig?: Record; + jsonConfig?: Record; +}): string { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brownfield-cli-config-')); + + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify( + { + name: 'brownfield-config-test', + version: '1.0.0', + 'react-native-brownfield': options?.packageJsonConfig, + }, + null, + 2 + ) + ); + + if (options?.jsConfig) { + fs.writeFileSync( + path.join(tempDir, 'react-native-brownfield.config.js'), + `module.exports = ${JSON.stringify(options.jsConfig, null, 2)};\n` + ); + } + + if (options?.jsonConfig) { + fs.writeFileSync( + path.join(tempDir, 'react-native-brownfield.config.json'), + JSON.stringify(options.jsonConfig, null, 2) + ); + } + + return tempDir; +} + +function cleanupTempDir(directory: string): void { + fs.rmSync(directory, { recursive: true, force: true }); +} + +describe('loadBrownfieldConfig', () => { + let tempDir: string | null = null; + + afterEach(() => { + mockLoggerWarn.mockReset(); + + if (tempDir) { + cleanupTempDir(tempDir); + tempDir = null; + } + }); + + it('prefers js config over json and package.json', () => { + tempDir = createTempProject({ + packageJsonConfig: { verbose: false, variant: 'package-json' }, + jsonConfig: { verbose: false, variant: 'json' }, + jsConfig: { verbose: true, variant: 'js' }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + verbose: true, + variant: 'js', + }); + }); + + it('prefers json config over package.json when js config is missing', () => { + tempDir = createTempProject({ + packageJsonConfig: { verbose: false, variant: 'package-json' }, + jsonConfig: { verbose: true, variant: 'json' }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + verbose: true, + variant: 'json', + }); + }); + + it('falls back to package.json config when js and json configs are missing', () => { + tempDir = createTempProject({ + packageJsonConfig: { verbose: true, variant: 'package-json' }, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({ + verbose: true, + variant: 'package-json', + }); + }); +}); + +describe('validateBrownfieldCLIConfig', () => { + afterEach(() => { + mockLoggerWarn.mockReset(); + }); + + it('does not warn for valid config', () => { + validateBrownfieldCLIConfig({ + verbose: true, + variant: 'release', + }); + + expect(mockLoggerWarn).not.toHaveBeenCalled(); + }); + + it('warns for schema violations', () => { + validateBrownfieldCLIConfig({ + unsupportedOption: true, + }); + + expect(mockLoggerWarn).toHaveBeenCalledOnce(); + expect(mockLoggerWarn.mock.calls[0]?.[0]).toContain( + 'Brownfield configuration has some issues:' + ); + expect(mockLoggerWarn.mock.calls[0]?.[0]).toContain( + 'should NOT have additional properties' + ); + }); +}); + +describe('config application', () => { + let tempDir: string | null = null; + + afterEach(() => { + mockLoggerWarn.mockReset(); + + if (tempDir) { + cleanupTempDir(tempDir); + tempDir = null; + } + }); + + it('applies config values to a commander program with config as the source', () => { + const program = new Command(); + + applyBrownfieldCLIConfig(program, { + verbose: true, + variant: 'release', + }); + + expect(program.getOptionValue('verbose')).toBe(true); + expect(program.getOptionValueSource('verbose')).toBe('config'); + expect(program.getOptionValue('variant')).toBe('release'); + expect(program.getOptionValueSource('variant')).toBe('config'); + }); + + it('loads config and attaches it to the commander program', () => { + tempDir = createTempProject({ + packageJsonConfig: { verbose: true, variant: 'release' }, + }); + + const program = new Command(); + + loadAndApplyBrownfieldCLIConfig(program, tempDir); + + expect(program.getOptionValue('verbose')).toBe(true); + expect(program.getOptionValueSource('verbose')).toBe('config'); + expect(program.getOptionValue('variant')).toBe('release'); + expect(program.getOptionValueSource('variant')).toBe('config'); + }); +}); \ No newline at end of file diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 4cbb2aca..2ee9c481 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -20,13 +20,13 @@ const SEPARATOR = '\n● '; const ajv = new Ajv({ allErrors: true }); const validateBrownfieldConfig = ajv.compile(BrownfieldSchema); -function validateConfig(config: unknown) { +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' })}.`); } } -function loadBrownfieldConfig( +export function loadBrownfieldConfig( projectRoot: string = findProjectRoot() ): BrownfieldConfig { const require = createRequire(path.join(projectRoot, 'package.json')); @@ -50,15 +50,23 @@ function loadBrownfieldConfig( return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; } +export function applyBrownfieldCLIConfig( + program: Command, + config: BrownfieldConfig +): void { + for (const [key, value] of Object.entries(config)) { + program.setOptionValueWithSource(key, value, 'config'); + } +} -export function loadAndApplyBrownfieldCLIConfig(program: Command) { - const reactNativeBrownfieldConfig = loadBrownfieldConfig() +export function loadAndApplyBrownfieldCLIConfig( + program: Command, + projectRoot?: string +): void { + const reactNativeBrownfieldConfig = loadBrownfieldConfig(projectRoot); logger.debug('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); - validateConfig(reactNativeBrownfieldConfig); - - for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { - program.setOptionValueWithSource(key, value, 'config'); - } + validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); + applyBrownfieldCLIConfig(program, reactNativeBrownfieldConfig); } \ No newline at end of file From f8596ee1f6f625bb3385bd9b23046ad34d294c87 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Fri, 22 May 2026 11:04:14 +0200 Subject: [PATCH 07/11] chore: add configs to apps and support package.json typeahead --- .vscode/settings.json | 8 + apps/ExpoApp54/package.json | 6 +- .../react-native-brownfield.config.json | 5 + apps/ExpoApp55/package.json | 11 +- packages/cli/package-json.schema.json | 11 ++ packages/cli/schema.json | 142 ++++++++++-------- 6 files changed, 112 insertions(+), 71 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 apps/ExpoApp54/react-native-brownfield.config.json create mode 100644 packages/cli/package-json.schema.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..dada16c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "json.schemas": [ + { + "fileMatch": ["**/package.json"], + "url": "./packages/cli/package-json.schema.json" + } + ] +} \ No newline at end of file diff --git a/apps/ExpoApp54/package.json b/apps/ExpoApp54/package.json index 0a85eaf7..587d6ba7 100644 --- a/apps/ExpoApp54/package.json +++ b/apps/ExpoApp54/package.json @@ -11,9 +11,9 @@ "lint": "expo lint", "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", - "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release" + "brownfield:package:android": "brownfield package:android --variant release", + "brownfield:publish:android": "brownfield publish:android", + "brownfield:package:ios": "brownfield package:ios --configuration Release" }, "dependencies": { "@callstack/brownfield-navigation": "workspace:^", diff --git a/apps/ExpoApp54/react-native-brownfield.config.json b/apps/ExpoApp54/react-native-brownfield.config.json new file mode 100644 index 00000000..78d51c21 --- /dev/null +++ b/apps/ExpoApp54/react-native-brownfield.config.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../packages/cli/schema.json", + "moduleName": "brownfieldlib", + "scheme": "BrownfieldLib" +} diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index 4dfb63e1..86c91ee7 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -10,9 +10,9 @@ "lint": "expo lint", "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", - "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib", - "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release" + "brownfield:package:android": "brownfield package:android --variant release", + "brownfield:publish:android": "brownfield publish:android", + "brownfield:package:ios": "brownfield package:ios --configuration Release" }, "dependencies": { "@callstack/brownfield-navigation": "workspace:^", @@ -55,5 +55,10 @@ "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", + "moduleName": "brownfieldlib", + "scheme": "BrownfieldLib" } } diff --git a/packages/cli/package-json.schema.json b/packages/cli/package-json.schema.json new file mode 100644 index 00000000..8380109d --- /dev/null +++ b/packages/cli/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": "./schema.json" + } + } +} \ No newline at end of file diff --git a/packages/cli/schema.json b/packages/cli/schema.json index e601cf42..1cd9185e 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -1,70 +1,82 @@ { - "$ref": "#/definitions/BrownfieldConfig", "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "BrownfieldConfig": { - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string" - }, - "archive": { - "type": "boolean" - }, - "buildFolder": { - "type": "string" - }, - "configuration": { - "type": "string" - }, - "destination": { - "items": { - "type": "string" - }, - "type": "array" - }, - "exportExtraParams": { - "items": { - "type": "string" - }, - "type": "array" - }, - "exportOptionsPlist": { - "type": "string" - }, - "extraParams": { - "items": { - "type": "string" - }, - "type": "array" - }, - "installPods": { - "type": "boolean" - }, - "local": { - "type": "boolean" - }, - "moduleName": { - "type": "string" - }, - "newArch": { - "type": "boolean" - }, - "scheme": { - "type": "string" - }, - "target": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "verbose": { - "type": "boolean" - } - }, - "type": "object" + "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." + }, + "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." + }, + "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." } } } - From 4d998f39bba028c479a2be31d8715c48fb2433af Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Fri, 22 May 2026 11:21:30 +0200 Subject: [PATCH 08/11] chore: rewire schemas --- .vscode/settings.json | 2 +- apps/ExpoApp54/react-native-brownfield.config.json | 2 +- {packages/cli => docs/docs/public}/package-json.schema.json | 2 +- {packages/cli => docs/docs/public}/schema.json | 0 packages/cli/package.json | 3 +-- 5 files changed, 4 insertions(+), 5 deletions(-) rename {packages/cli => docs/docs/public}/package-json.schema.json (78%) rename {packages/cli => docs/docs/public}/schema.json (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index dada16c9..d0e78bad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "json.schemas": [ { "fileMatch": ["**/package.json"], - "url": "./packages/cli/package-json.schema.json" + "url": "https://oss.callstack.com/react-native-brownfield/package-json.schema.json" } ] } \ No newline at end of file diff --git a/apps/ExpoApp54/react-native-brownfield.config.json b/apps/ExpoApp54/react-native-brownfield.config.json index 8e76a1fb..998365f6 100644 --- a/apps/ExpoApp54/react-native-brownfield.config.json +++ b/apps/ExpoApp54/react-native-brownfield.config.json @@ -1,5 +1,5 @@ { - "$schema": "../../packages/cli/schema.json", + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", "moduleName": "brownfieldlib", "scheme": "BrownfieldLib", "verbose": true diff --git a/packages/cli/package-json.schema.json b/docs/docs/public/package-json.schema.json similarity index 78% rename from packages/cli/package-json.schema.json rename to docs/docs/public/package-json.schema.json index 8380109d..6eb5552f 100644 --- a/packages/cli/package-json.schema.json +++ b/docs/docs/public/package-json.schema.json @@ -5,7 +5,7 @@ "type": "object", "properties": { "react-native-brownfield": { - "$ref": "./schema.json" + "$ref": "https://oss.callstack.com/react-native-brownfield/schema.json" } } } \ No newline at end of file diff --git a/packages/cli/schema.json b/docs/docs/public/schema.json similarity index 100% rename from packages/cli/schema.json rename to docs/docs/public/schema.json diff --git a/packages/cli/package.json b/packages/cli/package.json index e6f8d376..87d17118 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,8 +68,7 @@ "!**/__fixtures__", "!**/__mocks__", "!**/.*", - "README.md", - "schema.json" + "README.md" ], "publishConfig": { "access": "public" From 998c13dd17f15a0572486389334d6d3d6a1e1387 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Tue, 26 May 2026 13:14:32 +0200 Subject: [PATCH 09/11] feat: add brownie to the config --- apps/ExpoApp54/package.json | 4 - .../react-native-brownfield.config.json | 6 +- docs/docs/public/schema.json | 15 ++ packages/cli/schema.json | 97 ++++++++ packages/cli/src/__tests__/config.test.ts | 211 +++++++++++------- .../__tests__/commands/codegen.test.ts | 21 ++ packages/cli/src/brownie/commands/codegen.ts | 24 +- packages/cli/src/brownie/config.ts | 28 ++- packages/cli/src/brownie/index.ts | 1 + packages/cli/src/config.ts | 61 ++--- packages/cli/src/index.ts | 3 - .../cli/src/navigation/commands/codegen.ts | 4 +- .../src/shared/utils/__tests__/cli.test.ts | 21 +- packages/cli/src/shared/utils/cli.ts | 2 + packages/cli/src/types.ts | 12 +- 15 files changed, 385 insertions(+), 125 deletions(-) create mode 100644 packages/cli/schema.json diff --git a/apps/ExpoApp54/package.json b/apps/ExpoApp54/package.json index 0d894136..c6e76d1b 100644 --- a/apps/ExpoApp54/package.json +++ b/apps/ExpoApp54/package.json @@ -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 index 998365f6..d11c47e8 100644 --- a/apps/ExpoApp54/react-native-brownfield.config.json +++ b/apps/ExpoApp54/react-native-brownfield.config.json @@ -2,5 +2,9 @@ "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", "moduleName": "brownfieldlib", "scheme": "BrownfieldLib", - "verbose": true + "verbose": true, + "brownie": { + "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp54/Generated/", + "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp54" + } } diff --git a/docs/docs/public/schema.json b/docs/docs/public/schema.json index 1cd9185e..7d6144b6 100644 --- a/docs/docs/public/schema.json +++ b/docs/docs/public/schema.json @@ -17,6 +17,21 @@ "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." diff --git a/packages/cli/schema.json b/packages/cli/schema.json new file mode 100644 index 00000000..7d6144b6 --- /dev/null +++ b/packages/cli/schema.json @@ -0,0 +1,97 @@ +{ + "$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." + }, + "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 index 4c54f049..aa56b4ba 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -4,187 +4,246 @@ import path from 'node:path'; import * as rockTools from '@rock-js/tools'; import { Command } from 'commander'; -import { afterEach, describe, expect, it, Mock, vi } from 'vitest'; - -import { - applyBrownfieldCLIConfig, - loadAndApplyBrownfieldCLIConfig, - loadBrownfieldConfig, - validateBrownfieldCLIConfig, -} from '../config.js'; +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, - debug: vi.fn(), warn: vi.fn(), + debug: vi.fn(), + setVerbose: vi.fn(), }, }; }); -const mockLoggerWarn = rockTools.logger.warn as Mock; +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(options?: { +function createTempProject({ + packageJsonConfig, + jsConfig, + jsonConfig, +}: { packageJsonConfig?: Record; jsConfig?: Record; jsonConfig?: Record; -}): string { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brownfield-cli-config-')); +} = {}): 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( - { - name: 'brownfield-config-test', - version: '1.0.0', - 'react-native-brownfield': options?.packageJsonConfig, - }, - null, - 2 - ) + JSON.stringify(packageJson, null, 2) ); - if (options?.jsConfig) { + if (jsConfig !== undefined) { fs.writeFileSync( path.join(tempDir, 'react-native-brownfield.config.js'), - `module.exports = ${JSON.stringify(options.jsConfig, null, 2)};\n` + `module.exports = ${JSON.stringify(jsConfig, null, 2)};\n` ); } - if (options?.jsonConfig) { + if (jsonConfig !== undefined) { fs.writeFileSync( path.join(tempDir, 'react-native-brownfield.config.json'), - JSON.stringify(options.jsonConfig, null, 2) + JSON.stringify(jsonConfig, null, 2) ); } return tempDir; } -function cleanupTempDir(directory: string): void { - fs.rmSync(directory, { recursive: true, force: true }); +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(() => { - mockLoggerWarn.mockReset(); + process.chdir(originalCwd); if (tempDir) { - cleanupTempDir(tempDir); + fs.rmSync(tempDir, { recursive: true, force: true }); tempDir = null; } }); - it('prefers js config over json and package.json', () => { + it('loads config from package.json', () => { tempDir = createTempProject({ - packageJsonConfig: { verbose: false, variant: 'package-json' }, - jsonConfig: { verbose: false, variant: 'json' }, - jsConfig: { verbose: true, variant: 'js' }, + packageJsonConfig: { + scheme: 'PackageScheme', + destination: ['simulator'], + }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ - verbose: true, - variant: 'js', + scheme: 'PackageScheme', + destination: ['simulator'], }); }); - it('prefers json config over package.json when js config is missing', () => { + it('loads config from a JavaScript config file', () => { tempDir = createTempProject({ - packageJsonConfig: { verbose: false, variant: 'package-json' }, - jsonConfig: { verbose: true, variant: 'json' }, + jsConfig: { + scheme: 'JsScheme', + installPods: true, + }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ - verbose: true, - variant: 'json', + scheme: 'JsScheme', + installPods: true, }); }); - it('falls back to package.json config when js and json configs are missing', () => { + it('loads config from a JSON config file', () => { tempDir = createTempProject({ - packageJsonConfig: { verbose: true, variant: 'package-json' }, + jsonConfig: { + scheme: 'JsonScheme', + verbose: true, + }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ + scheme: 'JsonScheme', verbose: true, - variant: 'package-json', }); }); + + 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', () => { - afterEach(() => { - mockLoggerWarn.mockReset(); + beforeEach(() => { + vi.clearAllMocks(); }); - it('does not warn for valid config', () => { + it('does not warn for a schema-valid config', () => { validateBrownfieldCLIConfig({ + scheme: 'AppScheme', + destination: ['simulator'], verbose: true, - variant: 'release', }); expect(mockLoggerWarn).not.toHaveBeenCalled(); }); - it('warns for schema violations', () => { + it('warns for a schema-invalid config', () => { validateBrownfieldCLIConfig({ unsupportedOption: true, }); - expect(mockLoggerWarn).toHaveBeenCalledOnce(); + expect(mockLoggerWarn).toHaveBeenCalledTimes(1); expect(mockLoggerWarn.mock.calls[0]?.[0]).toContain( 'Brownfield configuration has some issues:' ); - expect(mockLoggerWarn.mock.calls[0]?.[0]).toContain( - 'should NOT have additional properties' - ); }); }); -describe('config application', () => { +describe('addBrownfieldConfig', () => { let tempDir: string | null = null; + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { - mockLoggerWarn.mockReset(); + process.chdir(originalCwd); if (tempDir) { - cleanupTempDir(tempDir); + fs.rmSync(tempDir, { recursive: true, force: true }); tempDir = null; } }); - it('applies config values to a commander program with config as the source', () => { - const program = new Command(); - - applyBrownfieldCLIConfig(program, { - verbose: true, - variant: 'release', + it('applies config values to undefined CLI options', () => { + tempDir = createTempProject({ + packageJsonConfig: { + scheme: 'ConfigScheme', + installPods: true, + destination: ['simulator'], + }, }); + process.chdir(tempDir); - expect(program.getOptionValue('verbose')).toBe(true); - expect(program.getOptionValueSource('verbose')).toBe('config'); - expect(program.getOptionValue('variant')).toBe('release'); - expect(program.getOptionValueSource('variant')).toBe('config'); + 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('loads config and attaches it to the commander program', () => { + it('warns and preserves the CLI value when it overrides the config', () => { tempDir = createTempProject({ - packageJsonConfig: { verbose: true, variant: 'release' }, + packageJsonConfig: { + scheme: 'ConfigScheme', + }, }); + process.chdir(tempDir); - const program = new Command(); + const command = createCommand(); + command.setOptionValue('scheme', 'CliScheme'); - loadAndApplyBrownfieldCLIConfig(program, tempDir); + addBrownfieldConfig(command); - expect(program.getOptionValue('verbose')).toBe(true); - expect(program.getOptionValueSource('verbose')).toBe('config'); - expect(program.getOptionValue('variant')).toBe('release'); - expect(program.getOptionValueSource('variant')).toBe('config'); + expect(command.optsWithGlobals().scheme).toBe('CliScheme'); + expect(mockLoggerWarn).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); 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..649e9392 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.' + ); + } + + if (legacyConfig) { + logger.warn( + 'You are using legacy Brownie configuration. Please migrate to the new configuration format. See the documentation for more details.' + ); + } + + 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 index 2ee9c481..e32e503f 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -22,7 +22,9 @@ 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' })}.`); + logger.warn( + `Brownfield configuration has some issues: ${SEPARATOR}${ajv.errorsText(validateBrownfieldConfig.errors, { separator: SEPARATOR, dataVar: 'config' })}.` + ); } } @@ -32,41 +34,50 @@ export function loadBrownfieldConfig( 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; } - const jsonConfigFilePath = path.join(projectRoot, JSON_CONFIG_FILE_NAME); if (fs.existsSync(jsonConfigFilePath)) { return require(jsonConfigFilePath) as BrownfieldConfig; } - const packageJsonPath = path.join(projectRoot, 'package.json'); - const packageJson = require(packageJsonPath) as Record< - string, - unknown - >; - return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; } -export function applyBrownfieldCLIConfig( - program: Command, - config: BrownfieldConfig -): void { - for (const [key, value] of Object.entries(config)) { - program.setOptionValueWithSource(key, value, 'config'); - } -} - -export function loadAndApplyBrownfieldCLIConfig( - program: Command, - projectRoot?: string -): void { - const reactNativeBrownfieldConfig = loadBrownfieldConfig(projectRoot); +export function addBrownfieldConfig(...args: any[]): void { + // Last argument is the current command instance + const command = args.at(-1) as Command; - logger.debug('Loaded Brownfield CLI config:', reactNativeBrownfieldConfig); + const reactNativeBrownfieldConfig = loadBrownfieldConfig(); validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); - applyBrownfieldCLIConfig(program, reactNativeBrownfieldConfig); -} \ No newline at end of file + + 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/index.ts b/packages/cli/src/index.ts index 682a48b3..867a3dbb 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,7 +14,6 @@ import brownieCommands, { import navigationCommands, { groupName as navigationCommandsGroupName, } from './navigation/index.js'; -import { loadAndApplyBrownfieldCLIConfig } from './config.js'; const program = new Command(); @@ -83,8 +82,6 @@ program.commandsGroup('Utility commands').helpCommand('help [command]'); export function runCLI(argv: string[]): void { program.parse(argv); - loadAndApplyBrownfieldCLIConfig(program); - if (!argv.slice(2).length) { program.outputHelp(); } 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 index 5cc7553b..fc7bd4af 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -17,6 +17,11 @@ export type BrownfieldConfigMetadata = Partial<{ $schema: string; }> +export interface BrownieConfig { + kotlin?: string; + kotlinPackageName?: string; +} + export type BrownfieldPackageAndroidOptions = BrownfieldCommonOptions & Partial export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & Partial export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial @@ -24,4 +29,9 @@ export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial & Partial export type BrownfieldIosConfig = Partial -export type BrownfieldConfig = BrownfieldConfigMetadata & BrownfieldCommonOptions & BrownfieldAndroidConfig & BrownfieldIosConfig +export type BrownfieldConfig = + & BrownfieldConfigMetadata + & BrownfieldCommonOptions + & BrownfieldAndroidConfig + & BrownfieldIosConfig + & { brownie?: BrownieConfig }; From e55b36f230ddd24f01508a29b0d6c2276f73c379 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Tue, 26 May 2026 14:07:30 +0200 Subject: [PATCH 10/11] docs: add documentation --- docs/docs/docs/api-reference/_meta.json | 5 + .../docs/docs/api-reference/configuration.mdx | 194 ++++++++++++++++++ docs/docs/docs/cli/brownfield.mdx | 6 +- docs/docs/docs/cli/brownie.mdx | 5 + docs/docs/docs/getting-started/android.mdx | 29 ++- docs/docs/docs/getting-started/ios.mdx | 27 ++- .../docs/docs/getting-started/quick-start.mdx | 1 + docs/docs/public/schema.json | 4 + packages/cli/schema.json | 4 + packages/cli/src/__tests__/config.test.ts | 6 + .../cli/src/brownfield/commands/packageIos.ts | 9 +- packages/cli/src/brownie/commands/codegen.ts | 4 +- packages/cli/src/types.ts | 48 ++--- 13 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 docs/docs/docs/api-reference/configuration.mdx 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/schema.json b/docs/docs/public/schema.json index 7d6144b6..93f25766 100644 --- a/docs/docs/public/schema.json +++ b/docs/docs/public/schema.json @@ -77,6 +77,10 @@ "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." diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 7d6144b6..93f25766 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -77,6 +77,10 @@ "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." diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index aa56b4ba..651c1361 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -172,7 +172,13 @@ describe('validateBrownfieldCLIConfig', () => { 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(); 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/commands/codegen.ts b/packages/cli/src/brownie/commands/codegen.ts index 649e9392..83fb1fd3 100644 --- a/packages/cli/src/brownie/commands/codegen.ts +++ b/packages/cli/src/brownie/commands/codegen.ts @@ -102,13 +102,13 @@ export async function runCodegen({ platform, brownie }: RunCodegenOptions) { 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.' + '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.' + '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' ); } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index fc7bd4af..9307c52e 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,37 +1,37 @@ -import { - type PackageAarFlags, -} from '@rock-js/platform-android'; +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'; +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 interface BrownieConfig { +export type BrownieConfig = { kotlin?: string; kotlinPackageName?: string; -} +}; -export type BrownfieldPackageAndroidOptions = BrownfieldCommonOptions & Partial -export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & Partial -export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial +export type PackageIosOptions = AppleBuildFlags & { + usePrebuiltRnCore?: boolean; +}; -export type BrownfieldAndroidConfig = Partial & Partial -export type BrownfieldIosConfig = Partial +export type BrownfieldPackageAndroidOptions = BrownfieldCommonOptions & + Partial; +export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & + Partial; +export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & + Partial; -export type BrownfieldConfig = - & BrownfieldConfigMetadata - & BrownfieldCommonOptions - & BrownfieldAndroidConfig - & BrownfieldIosConfig - & { brownie?: BrownieConfig }; +export type BrownfieldAndroidConfig = Partial & + Partial; +export type BrownfieldIosConfig = Partial; + +export type BrownfieldConfig = BrownfieldConfigMetadata & + BrownfieldCommonOptions & + BrownfieldAndroidConfig & + BrownfieldIosConfig & { brownie?: BrownieConfig }; From 81d21bf4c7aa4cc77f351889f02ede1d38065541 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Fri, 29 May 2026 12:05:24 +0200 Subject: [PATCH 11/11] chore: change brownie config --- apps/ExpoApp55/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index 033e9da9..c033f68a 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -62,12 +62,12 @@ "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