From 7e7602c63c8c5c21eff76e9a98f1d6629bafc0ce Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 18 May 2026 12:59:08 +0200 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 From fada2ba8d96387a97b0e9879679586b725b63b80 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 10 Jun 2026 13:10:04 +0200 Subject: [PATCH 12/20] fix: simplify rn-brownfield name --- ...e-brownfield.config.json => brownfield.config.json} | 0 apps/ExpoApp55/package.json | 2 +- ...ative-brownfield.config.js => brownfield.config.js} | 0 docs/docs/docs/api-reference/configuration.mdx | 4 ++-- docs/docs/public/package-json.schema.json | 2 +- packages/cli/src/__tests__/config.test.ts | 6 +++--- packages/cli/src/config.ts | 10 +++++----- 7 files changed, 12 insertions(+), 12 deletions(-) rename apps/ExpoApp54/{react-native-brownfield.config.json => brownfield.config.json} (100%) rename apps/RNApp/{react-native-brownfield.config.js => brownfield.config.js} (100%) diff --git a/apps/ExpoApp54/react-native-brownfield.config.json b/apps/ExpoApp54/brownfield.config.json similarity index 100% rename from apps/ExpoApp54/react-native-brownfield.config.json rename to apps/ExpoApp54/brownfield.config.json diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index c033f68a..83bf1b37 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -62,7 +62,7 @@ "typescript": "~5.9.2" }, "private": true, - "react-native-brownfield": { + "brownfield": { "$schema": "../../packages/cli/schema.json", "brownie": { "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", diff --git a/apps/RNApp/react-native-brownfield.config.js b/apps/RNApp/brownfield.config.js similarity index 100% rename from apps/RNApp/react-native-brownfield.config.js rename to apps/RNApp/brownfield.config.js diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx index ed4a3126..f4d243c1 100644 --- a/docs/docs/docs/api-reference/configuration.mdx +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -64,7 +64,7 @@ If you prefer to keep everything in `package.json`, place the configuration unde ```json { "name": "my-app", - "react-native-brownfield": { + "brownfield": { "moduleName": ":BrownfieldLib", "scheme": "BrownfieldLib", "verbose": true, @@ -157,7 +157,7 @@ The new format moves the same values under the main Brownfield config: ```json { - "react-native-brownfield": { + "brownfield": { "moduleName": ":BrownfieldLib", "scheme": "BrownfieldLib", "brownie": { diff --git a/docs/docs/public/package-json.schema.json b/docs/docs/public/package-json.schema.json index 6eb5552f..28383c8d 100644 --- a/docs/docs/public/package-json.schema.json +++ b/docs/docs/public/package-json.schema.json @@ -4,7 +4,7 @@ "description": "Adds react-native-brownfield configuration completions to package.json.", "type": "object", "properties": { - "react-native-brownfield": { + "brownfield": { "$ref": "https://oss.callstack.com/react-native-brownfield/schema.json" } } diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index 651c1361..702ef434 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -49,7 +49,7 @@ function createTempProject({ }; if (packageJsonConfig !== undefined) { - packageJson['react-native-brownfield'] = packageJsonConfig; + packageJson['brownfield'] = packageJsonConfig; } fs.writeFileSync( @@ -59,14 +59,14 @@ function createTempProject({ if (jsConfig !== undefined) { fs.writeFileSync( - path.join(tempDir, 'react-native-brownfield.config.js'), + path.join(tempDir, 'brownfield.config.js'), `module.exports = ${JSON.stringify(jsConfig, null, 2)};\n` ); } if (jsonConfig !== undefined) { fs.writeFileSync( - path.join(tempDir, 'react-native-brownfield.config.json'), + path.join(tempDir, 'brownfield.config.json'), JSON.stringify(jsonConfig, null, 2) ); } diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index e32e503f..6dbc2e36 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -11,9 +11,9 @@ import BrownfieldSchema from '../schema.json' with { type: 'json' }; import { logger } from '@rock-js/tools'; import { Command } from 'commander'; -const JS_CONFIG_FILE_NAME = 'react-native-brownfield.config.js'; -const JSON_CONFIG_FILE_NAME = 'react-native-brownfield.config.json'; -const PACKAGE_JSON_CONFIG_KEY = 'react-native-brownfield'; +const CONFIG_BASE_NAME = 'brownfield'; +const JS_CONFIG_FILE_NAME = `.${CONFIG_BASE_NAME}.config.js`; +const JSON_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.json`; const SEPARATOR = '\n● '; @@ -42,7 +42,7 @@ export function loadBrownfieldConfig( [ fs.existsSync(jsConfigFilePath), fs.existsSync(jsonConfigFilePath), - packageJson[PACKAGE_JSON_CONFIG_KEY], + packageJson[CONFIG_BASE_NAME], ].filter(Boolean).length > 1 ) { throw new Error('Project has multiple Brownfield configuration files'); @@ -56,7 +56,7 @@ export function loadBrownfieldConfig( return require(jsonConfigFilePath) as BrownfieldConfig; } - return packageJson[PACKAGE_JSON_CONFIG_KEY] || {}; + return packageJson[CONFIG_BASE_NAME] || {}; } export function addBrownfieldConfig(...args: any[]): void { From 696caf9e7fd3df562de77317467f04f88f0812c0 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 10 Jun 2026 13:46:16 +0200 Subject: [PATCH 13/20] fix: typo, import and ajv upgrade --- packages/cli/package.json | 2 +- packages/cli/src/brownfield/commands/packageIos.ts | 2 +- packages/cli/src/config.ts | 4 +++- yarn.lock | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 87d17118..4ede8b18 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -86,7 +86,7 @@ "@rock-js/plugin-brownfield-android": "^0.13.3", "@rock-js/plugin-brownfield-ios": "^0.13.3", "@rock-js/tools": "^0.13.3", - "ajv": "^6.14.0", + "ajv": "^8.20.0", "commander": "^14.0.3", "quicktype-core": "^23.2.6", "quicktype-typescript-input": "^23.2.6", diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts index 52d6f622..43d33656 100644 --- a/packages/cli/src/brownfield/commands/packageIos.ts +++ b/packages/cli/src/brownfield/commands/packageIos.ts @@ -27,7 +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'; +import type { 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 = diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 6dbc2e36..0f037111 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -12,7 +12,7 @@ import { logger } from '@rock-js/tools'; import { Command } from 'commander'; const CONFIG_BASE_NAME = 'brownfield'; -const JS_CONFIG_FILE_NAME = `.${CONFIG_BASE_NAME}.config.js`; +const JS_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.js`; const JSON_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.json`; const SEPARATOR = '\n● '; @@ -63,6 +63,8 @@ export function addBrownfieldConfig(...args: any[]): void { // Last argument is the current command instance const command = args.at(-1) as Command; + if (!(command instanceof Command)) return; + const reactNativeBrownfieldConfig = loadBrownfieldConfig(); validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); diff --git a/yarn.lock b/yarn.lock index fab24487..17885ed7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1688,7 +1688,7 @@ __metadata: "@types/babel__preset-env": "npm:^7.10.0" "@types/node": "npm:^25.5.0" "@vitest/coverage-v8": "npm:^4.1.0" - ajv: "npm:^6.14.0" + ajv: "npm:^8.20.0" commander: "npm:^14.0.3" eslint: "npm:^9.39.3" globals: "npm:^17.3.0" @@ -7411,7 +7411,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.11.0": +"ajv@npm:^8.11.0, ajv@npm:^8.20.0": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: From 217a3f0e8040d4dee77f2a26b1871eaed2b4cf36 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 10 Jun 2026 13:54:30 +0200 Subject: [PATCH 14/20] fix: imports and findRoot consolidation --- packages/cli/src/brownie/config.ts | 13 ++++++++----- packages/cli/src/types.ts | 9 +++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/brownie/config.ts b/packages/cli/src/brownie/config.ts index 2965e73c..293cb535 100644 --- a/packages/cli/src/brownie/config.ts +++ b/packages/cli/src/brownie/config.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; +import { findProjectRoot } from '../brownfield/utils/paths'; export interface BrownieConfig { kotlin?: string; @@ -11,7 +12,7 @@ interface PackageJson { brownie?: BrownieConfig; } -function loadPackageJson(projectRoot: string = process.cwd()): PackageJson { +function loadPackageJson(projectRoot: string = findProjectRoot()): PackageJson { const packageJsonPath = path.resolve(projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { @@ -25,7 +26,7 @@ function loadPackageJson(projectRoot: string = process.cwd()): PackageJson { * Checks if @callstack/brownie package is installed. */ export function isBrownieInstalled( - projectRoot: string = process.cwd() + projectRoot: string = findProjectRoot() ): boolean { const require = createRequire(path.join(projectRoot, 'package.json')); try { @@ -40,7 +41,7 @@ export function isBrownieInstalled( * Resolves the path to the @callstack/brownie package. */ export function getBrowniePackagePath( - projectRoot: string = process.cwd() + projectRoot: string = findProjectRoot() ): string { const require = createRequire(path.join(projectRoot, 'package.json')); try { @@ -58,7 +59,7 @@ export function getBrowniePackagePath( * Returns the output path for generated Swift files. */ export function getSwiftOutputPath( - projectRoot: string = process.cwd() + projectRoot: string = findProjectRoot() ): string { const browniePath = getBrowniePackagePath(projectRoot); return path.join(browniePath, 'ios', 'Generated'); @@ -67,7 +68,9 @@ export function getSwiftOutputPath( /** * Returns whether package.json contains legacy brownie config. */ -export function hasLegacyConfig(projectRoot: string = process.cwd()): boolean { +export function hasLegacyConfig( + projectRoot: string = findProjectRoot() +): boolean { const packageJson = loadPackageJson(projectRoot); return Object.prototype.hasOwnProperty.call(packageJson, 'brownie'); diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 9307c52e..af47e002 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,7 +1,8 @@ -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, + PackageAarFlags, +} from '@rock-js/platform-android'; +import type { BuildFlags as AppleBuildFlags } from '@rock-js/platform-apple-helpers'; export type BrownfieldCommonOptions = Partial<{ verbose: boolean; From c0c7096539e041a071436a51ec5fa1d05809ea53 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 10 Jun 2026 14:33:07 +0200 Subject: [PATCH 15/20] fix: type consolitation, schema consolidation --- .gitignore | 2 +- apps/ExpoApp55/package.json | 1 - docs/.gitignore | 3 + docs/docs/public/schema.json | 101 ------------------ docs/package.json | 5 +- docs/scripts/copy-schema.mjs | 11 ++ packages/cli/src/brownie/commands/codegen.ts | 9 +- packages/cli/src/brownie/config.ts | 6 +- .../helpers/runBrownieCodegenIfApplicable.ts | 2 +- packages/cli/src/brownie/index.ts | 2 - packages/cli/src/brownie/types.ts | 1 - packages/cli/src/types.ts | 2 + 12 files changed, 24 insertions(+), 121 deletions(-) delete mode 100644 docs/docs/public/schema.json create mode 100644 docs/scripts/copy-schema.mjs delete mode 100644 packages/cli/src/brownie/types.ts diff --git a/.gitignore b/.gitignore index e7169098..369a9406 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,4 @@ secring.gpg # skillgym .skillgym-results/ -.cursor \ No newline at end of file +.cursor diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index 83bf1b37..00d70ebb 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -63,7 +63,6 @@ }, "private": true, "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" diff --git a/docs/.gitignore b/docs/.gitignore index 044373fb..4861b320 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -23,3 +23,6 @@ dist-ssr *.sln *.sw? doc_build + +# Copied schema file +docs/public/schema.json diff --git a/docs/docs/public/schema.json b/docs/docs/public/schema.json deleted file mode 100644 index 93f25766..00000000 --- a/docs/docs/public/schema.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "React Native Brownfield CLI config", - "description": "Configuration for react-native-brownfield.config.json and package.json#react-native-brownfield.", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema reference for editor tooling" - }, - "archive": { - "type": "boolean", - "description": "Create an Xcode archive (IPA) of the build, required for uploading to App Store Connect or distributing to TestFlight." - }, - "buildFolder": { - "type": "string", - "description": "Location for iOS build artifacts. Corresponds to Xcode's \"-derivedDataPath\". By default, the '/.brownfield/build' path is used." - }, - "brownie": { - "type": "object", - "description": "Configuration for Brownie code generation. Use this nested object in react-native-brownfield config instead of the legacy package.json#brownie block.", - "additionalProperties": false, - "properties": { - "kotlin": { - "type": "string", - "description": "Directory where generated Kotlin Brownie store files should be written." - }, - "kotlinPackageName": { - "type": "string", - "description": "Kotlin package name used in generated Brownie store files." - } - } - }, - "configuration": { - "type": "string", - "description": "Explicitly set the scheme configuration to use. This option is case sensitive." - }, - "destination": { - "type": "array", - "description": "Define destination values for the build. You can pass multiple destinations as separate values. Supported values include \"simulator\", \"device\", or full xcodebuild destination strings.", - "items": { - "type": "string" - } - }, - "exportExtraParams": { - "type": "array", - "description": "Custom params passed to the xcodebuild export archive command.", - "items": { - "type": "string" - } - }, - "exportOptionsPlist": { - "type": "string", - "description": "Name of the export options file for archiving. Defaults to ExportOptions.plist." - }, - "extraParams": { - "type": "array", - "description": "Custom params passed to the xcodebuild command.", - "items": { - "type": "string" - } - }, - "installPods": { - "type": "boolean", - "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." - }, - "local": { - "type": "boolean", - "description": "Force a local build with xcodebuild." - }, - "moduleName": { - "type": "string", - "description": "AAR module name." - }, - "newArch": { - "type": "boolean", - "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." - }, - "usePrebuiltRnCore": { - "type": "boolean", - "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." - }, - "scheme": { - "type": "string", - "description": "Explicitly set the Xcode scheme to use." - }, - "target": { - "type": "string", - "description": "Explicitly set the Xcode target to use." - }, - "variant": { - "type": "string", - "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." - }, - "verbose": { - "type": "boolean", - "description": "Enable verbose logging." - } - } -} diff --git a/docs/package.json b/docs/package.json index 6f750350..4550bc83 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,8 +7,9 @@ "hoistingLimits": "workspaces" }, "scripts": { - "dev": "rspress dev", - "build": "rspress build", + "copy:schema": "node ./scripts/copy-schema.mjs", + "dev": "yarn run copy:schema && rspress dev", + "build": "yarn run copy:schema && rspress build", "build:docs": "yarn run build", "preview": "rspress preview" }, diff --git a/docs/scripts/copy-schema.mjs b/docs/scripts/copy-schema.mjs new file mode 100644 index 00000000..81dd3b59 --- /dev/null +++ b/docs/scripts/copy-schema.mjs @@ -0,0 +1,11 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..', '..'); + +const sourcePath = path.join(repoRoot, 'packages', 'cli', 'schema.json'); +const targetPath = path.join(repoRoot, 'docs', 'docs', 'public', 'schema.json'); + +await fs.copyFile(sourcePath, targetPath); diff --git a/packages/cli/src/brownie/commands/codegen.ts b/packages/cli/src/brownie/commands/codegen.ts index 83fb1fd3..1571b8e9 100644 --- a/packages/cli/src/brownie/commands/codegen.ts +++ b/packages/cli/src/brownie/commands/codegen.ts @@ -6,17 +6,12 @@ import { Command, Option } from 'commander'; 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, -} from '../config.js'; +import { hasLegacyConfig, loadConfig, getSwiftOutputPath } from '../config.js'; import { generateSwift } from '../generators/swift.js'; import { generateKotlin } from '../generators/kotlin.js'; import { discoverStores, type DiscoveredStore } from '../store-discovery.js'; -import type { Platform } from '../types.js'; import { NoBrownieStoresError } from '../errors/NoBrownieStoresError.js'; +import type { BrownieConfig, Platform } from '../../types.js'; function getOutputPath(dir: string, name: string, ext: string): string { return path.join(dir, `${name}.${ext}`); diff --git a/packages/cli/src/brownie/config.ts b/packages/cli/src/brownie/config.ts index 293cb535..5c57d34c 100644 --- a/packages/cli/src/brownie/config.ts +++ b/packages/cli/src/brownie/config.ts @@ -2,11 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { findProjectRoot } from '../brownfield/utils/paths'; - -export interface BrownieConfig { - kotlin?: string; - kotlinPackageName?: string; -} +import type { BrownieConfig } from '../types.js'; interface PackageJson { brownie?: BrownieConfig; diff --git a/packages/cli/src/brownie/helpers/runBrownieCodegenIfApplicable.ts b/packages/cli/src/brownie/helpers/runBrownieCodegenIfApplicable.ts index 86b947e9..fd92e8b6 100644 --- a/packages/cli/src/brownie/helpers/runBrownieCodegenIfApplicable.ts +++ b/packages/cli/src/brownie/helpers/runBrownieCodegenIfApplicable.ts @@ -1,7 +1,7 @@ import { runCodegen } from '../commands/codegen.js'; import { isBrownieInstalled } from '../config.js'; -import type { Platform } from '../types.js'; +import type { Platform } from '../../types.js'; export async function runBrownieCodegenIfApplicable( projectRoot: string, diff --git a/packages/cli/src/brownie/index.ts b/packages/cli/src/brownie/index.ts index b3377616..c7dfece7 100644 --- a/packages/cli/src/brownie/index.ts +++ b/packages/cli/src/brownie/index.ts @@ -2,11 +2,9 @@ import { styleText } from 'node:util'; import * as Commands from './commands/index.js'; -export type * from './types.js'; 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/brownie/types.ts b/packages/cli/src/brownie/types.ts deleted file mode 100644 index 7bafef24..00000000 --- a/packages/cli/src/brownie/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Platform = 'swift' | 'kotlin'; diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index af47e002..d770bacb 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -4,6 +4,8 @@ import type { } from '@rock-js/platform-android'; import type { BuildFlags as AppleBuildFlags } from '@rock-js/platform-apple-helpers'; +export type Platform = 'swift' | 'kotlin'; + export type BrownfieldCommonOptions = Partial<{ verbose: boolean; }>; From 6b0f191b339fd3ec0f0c2be257183c55af10382c Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 17 Jun 2026 12:28:38 +0200 Subject: [PATCH 16/20] chore: extract configs to android/ios keys --- apps/ExpoApp54/brownfield.config.json | 8 +- apps/ExpoApp55/package.json | 8 +- apps/RNApp/brownfield.config.js | 8 +- .../docs/docs/api-reference/configuration.mdx | 87 ++++++---- docs/docs/docs/getting-started/android.mdx | 6 +- docs/docs/docs/getting-started/ios.mdx | 6 +- packages/cli/schema.json | 150 ++++++++++-------- packages/cli/src/__tests__/config.test.ts | 139 +++++++++++----- .../src/brownfield/commands/packageAndroid.ts | 11 +- .../cli/src/brownfield/commands/packageIos.ts | 5 +- .../src/brownfield/commands/publishAndroid.ts | 5 +- packages/cli/src/brownie/config.ts | 2 +- packages/cli/src/config.ts | 38 ++--- .../src/shared/utils/__tests__/cli.test.ts | 15 -- packages/cli/src/shared/utils/cli.ts | 8 +- packages/cli/src/types.ts | 7 +- 16 files changed, 304 insertions(+), 199 deletions(-) diff --git a/apps/ExpoApp54/brownfield.config.json b/apps/ExpoApp54/brownfield.config.json index d11c47e8..a836f915 100644 --- a/apps/ExpoApp54/brownfield.config.json +++ b/apps/ExpoApp54/brownfield.config.json @@ -1,7 +1,11 @@ { "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", - "moduleName": "brownfieldlib", - "scheme": "BrownfieldLib", + "android": { + "moduleName": "brownfieldlib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, "verbose": true, "brownie": { "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp54/Generated/", diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index 00d70ebb..c1070183 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -67,8 +67,12 @@ "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp55" }, - "moduleName": "brownfieldlib", - "scheme": "BrownfieldLib", + "android": { + "moduleName": "brownfieldlib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, "verbose": true } } diff --git a/apps/RNApp/brownfield.config.js b/apps/RNApp/brownfield.config.js index 051eec50..2304c089 100644 --- a/apps/RNApp/brownfield.config.js +++ b/apps/RNApp/brownfield.config.js @@ -2,7 +2,11 @@ * @type {import('@callstack/react-native-brownfield').BrownfieldConfig} */ module.exports = { - moduleName: ':BrownfieldLib', - scheme: 'BrownfieldLib', + android: { + moduleName: ':BrownfieldLib', + }, + ios: { + scheme: 'BrownfieldLib', + }, verbose: true, }; diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx index f4d243c1..b24783d2 100644 --- a/docs/docs/docs/api-reference/configuration.mdx +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -3,7 +3,7 @@ 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. +Configuration keys use camelCase names that match CLI flags inside their platform section. For example, `--module-name` becomes `moduleName`, `--build-folder` becomes `buildFolder`, and `--use-prebuilt-rn-core` becomes `usePrebuiltRnCore`. ## Choose one configuration source @@ -27,9 +27,13 @@ If you prefer a JavaScript file, create `react-native-brownfield.config.js` and ```js /** @type {import('@callstack/react-native-brownfield').BrownfieldConfig} */ module.exports = { - moduleName: ':BrownfieldLib', - scheme: 'BrownfieldLib', verbose: true, + android: { + moduleName: ':BrownfieldLib', + }, + ios: { + scheme: 'BrownfieldLib', + }, brownie: { kotlin: './android/BrownfieldLib/src/main/java/com/example/brownfield/Generated/', @@ -45,11 +49,15 @@ If you want schema autocomplete and validation directly in the config file, use ```json { "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", - "moduleName": "brownfieldlib", - "scheme": "BrownfieldLib", - "configuration": "Release", "verbose": true, - "usePrebuiltRnCore": true, + "android": { + "moduleName": "brownfieldlib" + }, + "ios": { + "scheme": "BrownfieldLib", + "configuration": "Release", + "usePrebuiltRnCore": true + }, "brownie": { "kotlin": "./android/brownfieldlib/src/main/java/com/example/brownfield/Generated/", "kotlinPackageName": "com.example.brownfield" @@ -65,9 +73,13 @@ If you prefer to keep everything in `package.json`, place the configuration unde { "name": "my-app", "brownfield": { - "moduleName": ":BrownfieldLib", - "scheme": "BrownfieldLib", "verbose": true, + "android": { + "moduleName": ":BrownfieldLib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, "brownie": { "kotlin": "./android/BrownfieldLib/src/main/java/com/example/brownfield/Generated/", "kotlinPackageName": "com.example.brownfield" @@ -78,7 +90,7 @@ If you prefer to keep everything in `package.json`, place the configuration unde ## Configuration reference -All file-based options mirror CLI flags, but they use camelCase property names. +All file-based platform options mirror CLI flags, but they use camelCase property names under `android` or `ios`. ### Shared keys @@ -90,28 +102,29 @@ All file-based options mirror CLI flags, but they use camelCase property names. ### 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`. | +| Key | Type | Description | +| -------------------- | -------- | -------------------------------------------------------------------- | +| `android.moduleName` | `string` | Android module name used for packaging and publishing AAR artifacts. | +| `android.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. | +| Key | Type | Description | +| ------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------- | +| `ios.scheme` | `string` | Xcode scheme used for packaging. | +| `ios.configuration` | `string` | Xcode build configuration, for example `Debug` or `Release`. | +| `ios.target` | `string` | Explicit Xcode target name. | +| `ios.destination` | `string[]` | One or more Xcode destinations, such as `simulator`, `device`, or full destination strings. | +| `ios.buildFolder` | `string` | Custom build output directory. By default, Brownfield uses the `.brownfield/build` path inside the iOS project. | +| `ios.archive` | `boolean` | Creates an archive build suitable for IPA export and distribution. | +| `ios.extraParams` | `string[]` | Extra arguments passed to `xcodebuild`. | +| `ios.exportExtraParams` | `string[]` | Extra arguments passed to the archive export step. | +| `ios.exportOptionsPlist` | `string` | Export options plist filename used during archive export. | +| `ios.installPods` | `boolean` | Controls automatic CocoaPods installation. Set `false` to match `--no-install-pods`. | +| `ios.newArch` | `boolean` | Controls React Native new architecture support. Set `false` to match `--no-new-arch`. | +| `ios.local` | `boolean` | Forces a local `xcodebuild` flow. | +| `ios.usePrebuiltRnCore` | `boolean` | Controls whether iOS packaging uses React Native Apple prebuilts. Omit it to keep Brownfield's version-aware defaults. | +| `ios.addSpmPackage` | `boolean` | Generates a local Swift Package Manager manifest next to the packaged XCFramework outputs. | ## Brownie configuration @@ -158,8 +171,12 @@ The new format moves the same values under the main Brownfield config: ```json { "brownfield": { - "moduleName": ":BrownfieldLib", - "scheme": "BrownfieldLib", + "android": { + "moduleName": ":BrownfieldLib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, "brownie": { "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", "kotlinPackageName": "com.rnapp.brownfieldlib" @@ -173,8 +190,12 @@ You can also migrate to a standalone config file: ```json { "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", - "moduleName": ":BrownfieldLib", - "scheme": "BrownfieldLib", + "android": { + "moduleName": ":BrownfieldLib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, "brownie": { "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/", "kotlinPackageName": "com.rnapp.brownfieldlib" diff --git a/docs/docs/docs/getting-started/android.mdx b/docs/docs/docs/getting-started/android.mdx index 72f79596..488b69da 100644 --- a/docs/docs/docs/getting-started/android.mdx +++ b/docs/docs/docs/getting-started/android.mdx @@ -288,8 +288,10 @@ 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" + "android": { + "moduleName": "reactnativeapp", + "variant": "Release" + } } ``` diff --git a/docs/docs/docs/getting-started/ios.mdx b/docs/docs/docs/getting-started/ios.mdx index 70bdd2a5..9ac0cf5b 100644 --- a/docs/docs/docs/getting-started/ios.mdx +++ b/docs/docs/docs/getting-started/ios.mdx @@ -112,8 +112,10 @@ Create `react-native-brownfield.config.json` in your project root: ```json { "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", - "scheme": "", - "configuration": "Release" + "ios": { + "scheme": "", + "configuration": "Release" + } } ``` diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 93f25766..175eea7b 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -9,13 +9,20 @@ "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." + "android": { + "type": "object", + "description": "Android-specific Brownfield CLI configuration.", + "additionalProperties": false, + "properties": { + "moduleName": { + "type": "string", + "description": "AAR module name." + }, + "variant": { + "type": "string", + "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." + } + } }, "brownie": { "type": "object", @@ -32,67 +39,78 @@ } } }, - "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" + "ios": { + "type": "object", + "description": "iOS-specific Brownfield CLI configuration.", + "additionalProperties": false, + "properties": { + "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." + }, + "newArch": { + "type": "boolean", + "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." + }, + "usePrebuiltRnCore": { + "type": "boolean", + "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." + }, + "scheme": { + "type": "string", + "description": "Explicitly set the Xcode scheme to use." + }, + "target": { + "type": "string", + "description": "Explicitly set the Xcode target to use." + }, + "addSpmPackage": { + "type": "boolean", + "description": "Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs." + } } }, - "installPods": { - "type": "boolean", - "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." - }, - "local": { - "type": "boolean", - "description": "Force a local build with xcodebuild." - }, - "moduleName": { - "type": "string", - "description": "AAR module name." - }, - "newArch": { - "type": "boolean", - "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." - }, - "usePrebuiltRnCore": { - "type": "boolean", - "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." - }, - "scheme": { - "type": "string", - "description": "Explicitly set the Xcode scheme to use." - }, - "target": { - "type": "string", - "description": "Explicitly set the Xcode target to use." - }, - "variant": { - "type": "string", - "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." - }, "verbose": { "type": "boolean", "description": "Enable verbose logging." diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index 702ef434..c9315092 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -3,7 +3,6 @@ import os from 'node:os'; import path from 'node:path'; import * as rockTools from '@rock-js/tools'; -import { Command } from 'commander'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@rock-js/tools', async (importOriginal) => { @@ -24,8 +23,8 @@ vi.mock('../brownfield/utils/paths.js', () => ({ })); import { - addBrownfieldConfig, loadBrownfieldConfig, + mergeBrownfieldConfigWithOptions, validateBrownfieldCLIConfig, } from '../config.js'; @@ -74,15 +73,6 @@ function createTempProject({ return tempDir; } -function createCommand(): Command { - return new Command() - .option('--scheme ') - .option('--install-pods') - .option('--destination ') - .option('--target ') - .option('--extra-params '); -} - describe('loadBrownfieldConfig', () => { let tempDir: string | null = null; @@ -102,41 +92,53 @@ describe('loadBrownfieldConfig', () => { it('loads config from package.json', () => { tempDir = createTempProject({ packageJsonConfig: { - scheme: 'PackageScheme', - destination: ['simulator'], + ios: { + scheme: 'PackageScheme', + destination: ['simulator'], + }, }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ - scheme: 'PackageScheme', - destination: ['simulator'], + ios: { + scheme: 'PackageScheme', + destination: ['simulator'], + }, }); }); it('loads config from a JavaScript config file', () => { tempDir = createTempProject({ jsConfig: { - scheme: 'JsScheme', - installPods: true, + ios: { + scheme: 'JsScheme', + installPods: true, + }, }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ - scheme: 'JsScheme', - installPods: true, + ios: { + scheme: 'JsScheme', + installPods: true, + }, }); }); it('loads config from a JSON config file', () => { tempDir = createTempProject({ jsonConfig: { - scheme: 'JsonScheme', + ios: { + scheme: 'JsonScheme', + }, verbose: true, }, }); expect(loadBrownfieldConfig(tempDir)).toEqual({ - scheme: 'JsonScheme', + ios: { + scheme: 'JsonScheme', + }, verbose: true, }); }); @@ -150,10 +152,14 @@ describe('loadBrownfieldConfig', () => { it('throws when multiple config sources are present', () => { tempDir = createTempProject({ packageJsonConfig: { - scheme: 'PackageScheme', + ios: { + scheme: 'PackageScheme', + }, }, jsConfig: { - scheme: 'JsScheme', + ios: { + scheme: 'JsScheme', + }, }, }); @@ -170,10 +176,12 @@ describe('validateBrownfieldCLIConfig', () => { it('does not warn for a schema-valid config', () => { validateBrownfieldCLIConfig({ - scheme: 'AppScheme', - destination: ['simulator'], - usePrebuiltRnCore: true, verbose: true, + ios: { + scheme: 'AppScheme', + destination: ['simulator'], + usePrebuiltRnCore: true, + }, brownie: { kotlin: './android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/', @@ -196,7 +204,7 @@ describe('validateBrownfieldCLIConfig', () => { }); }); -describe('addBrownfieldConfig', () => { +describe('mergeBrownfieldConfigWithOptions', () => { let tempDir: string | null = null; beforeEach(() => { @@ -212,22 +220,26 @@ describe('addBrownfieldConfig', () => { } }); - it('applies config values to undefined CLI options', () => { + it('applies platform config values to undefined CLI options', () => { tempDir = createTempProject({ packageJsonConfig: { - scheme: 'ConfigScheme', - installPods: true, - destination: ['simulator'], + ios: { + scheme: 'ConfigScheme', + installPods: true, + destination: ['simulator'], + }, }, }); process.chdir(tempDir); - const command = createCommand(); - command.setOptionValue('target', 'MyApp'); + const options = { + target: 'MyApp', + scheme: undefined, + }; - addBrownfieldConfig(command); + const mergedOptions = mergeBrownfieldConfigWithOptions(options, 'ios'); - expect(command.optsWithGlobals()).toMatchObject({ + expect(mergedOptions).toMatchObject({ scheme: 'ConfigScheme', installPods: true, destination: ['simulator'], @@ -236,20 +248,63 @@ describe('addBrownfieldConfig', () => { expect(mockLoggerWarn).not.toHaveBeenCalled(); }); - it('warns and preserves the CLI value when it overrides the config', () => { + it('preserves CLI options when they override platform config', () => { tempDir = createTempProject({ packageJsonConfig: { - scheme: 'ConfigScheme', + ios: { + scheme: 'ConfigScheme', + }, }, }); process.chdir(tempDir); - const command = createCommand(); - command.setOptionValue('scheme', 'CliScheme'); + const options = { + scheme: 'CliScheme', + }; - addBrownfieldConfig(command); + const mergedOptions = mergeBrownfieldConfigWithOptions(options, 'ios'); - expect(command.optsWithGlobals().scheme).toBe('CliScheme'); - expect(mockLoggerWarn).toHaveBeenCalled(); + expect(mergedOptions.scheme).toBe('CliScheme'); + expect(mockLoggerWarn).not.toHaveBeenCalled(); + }); + + it('does not allow undefined options to override platform config', () => { + tempDir = createTempProject({ + packageJsonConfig: { + android: { + variant: 'release', + }, + }, + }); + process.chdir(tempDir); + + const options = { + variant: undefined, + }; + + const mergedOptions = mergeBrownfieldConfigWithOptions(options, 'android'); + + expect(mergedOptions.variant).toBe('release'); + }); + + it('applies shared config values to platform commands', () => { + tempDir = createTempProject({ + packageJsonConfig: { + verbose: true, + android: { + moduleName: ':BrownfieldLib', + }, + }, + }); + process.chdir(tempDir); + + const options = {}; + + const mergedOptions = mergeBrownfieldConfigWithOptions(options, 'android'); + + expect(mergedOptions).toMatchObject({ + verbose: true, + moduleName: ':BrownfieldLib', + }); }); }); diff --git a/packages/cli/src/brownfield/commands/packageAndroid.ts b/packages/cli/src/brownfield/commands/packageAndroid.ts index 6852f3c5..abd69b07 100644 --- a/packages/cli/src/brownfield/commands/packageAndroid.ts +++ b/packages/cli/src/brownfield/commands/packageAndroid.ts @@ -15,16 +15,15 @@ import { runExpoPrebuildIfNeeded } from '../utils/expo.js'; import { getProjectInfo } from '../utils/project.js'; import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js'; import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js'; +import { mergeBrownfieldConfigWithOptions } from '../../config.js'; export const packageAndroidCommand = curryOptions( new Command('package:android').description('Build Android AAR'), - packageAarOptions.map((option) => - option.name.startsWith('--variant') - ? { ...option, default: 'debug' } - : option - ) + packageAarOptions ).action( - actionRunner(async (options: PackageAarFlags) => { + actionRunner(async (cliOptions: PackageAarFlags) => { + const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android'); + const { projectRoot, platformConfig } = getProjectInfo('android'); await runExpoPrebuildIfNeeded({ projectRoot, diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts index 8637b4b4..cde328d7 100644 --- a/packages/cli/src/brownfield/commands/packageIos.ts +++ b/packages/cli/src/brownfield/commands/packageIos.ts @@ -31,6 +31,7 @@ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkN import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js'; import type { PackageIosOptions } from '../../types.js'; import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js'; +import { mergeBrownfieldConfigWithOptions } from '../../config.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 = @@ -94,7 +95,9 @@ export const packageIosCommand = curryOptions( ) ) .action( - actionRunner(async (options: PackageIosOptions) => { + actionRunner(async (cliOptions: PackageIosOptions) => { + const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios'); + const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios'); const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot }); diff --git a/packages/cli/src/brownfield/commands/publishAndroid.ts b/packages/cli/src/brownfield/commands/publishAndroid.ts index 6edb6746..dee9d60e 100644 --- a/packages/cli/src/brownfield/commands/publishAndroid.ts +++ b/packages/cli/src/brownfield/commands/publishAndroid.ts @@ -15,6 +15,7 @@ import { getProjectInfo } from '../utils/project.js'; import { runExpoPrebuildIfNeeded } from '../utils/expo.js'; import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js'; import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js'; +import { mergeBrownfieldConfigWithOptions } from '../../config.js'; export const publishAndroidCommand = curryOptions( new Command('publish:android').description( @@ -22,7 +23,9 @@ export const publishAndroidCommand = curryOptions( ), publishLocalAarOptions ).action( - actionRunner(async (options: PublishLocalAarFlags) => { + actionRunner(async (cliOptions: PublishLocalAarFlags) => { + const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android'); + const { projectRoot, platformConfig } = getProjectInfo('android'); await runExpoPrebuildIfNeeded({ projectRoot, diff --git a/packages/cli/src/brownie/config.ts b/packages/cli/src/brownie/config.ts index 5c57d34c..2945f3fe 100644 --- a/packages/cli/src/brownie/config.ts +++ b/packages/cli/src/brownie/config.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; -import { findProjectRoot } from '../brownfield/utils/paths'; +import { findProjectRoot } from '../brownfield/utils/paths.js'; import type { BrownieConfig } from '../types.js'; interface PackageJson { diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 0f037111..bd92eab8 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -9,7 +9,6 @@ import { findProjectRoot } from './brownfield/utils/paths.js'; import BrownfieldSchema from '../schema.json' with { type: 'json' }; import { logger } from '@rock-js/tools'; -import { Command } from 'commander'; const CONFIG_BASE_NAME = 'brownfield'; const JS_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.js`; @@ -59,27 +58,30 @@ export function loadBrownfieldConfig( return packageJson[CONFIG_BASE_NAME] || {}; } -export function addBrownfieldConfig(...args: any[]): void { - // Last argument is the current command instance - const command = args.at(-1) as Command; +type BrownfieldPlatform = 'android' | 'ios'; +type ConfigurableOptions = Record; - if (!(command instanceof Command)) return; +function getSharedConfig(config: BrownfieldConfig): ConfigurableOptions { + return config.verbose === undefined ? {} : { verbose: config.verbose }; +} +export function mergeBrownfieldConfigWithOptions( + options: T, + platform: BrownfieldPlatform +): T { const reactNativeBrownfieldConfig = loadBrownfieldConfig(); validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); - for (const [key, value] of Object.entries(reactNativeBrownfieldConfig)) { - const cliOptionValue = command.optsWithGlobals()[key]; - - if (cliOptionValue !== undefined) { - logger.warn( - 'CLI option "%s" is overriding the react-native-brownfield config value.', - key - ); - continue; - } - - command.setOptionValue(key, value); - } + const platformConfig = { + ...getSharedConfig(reactNativeBrownfieldConfig), + ...reactNativeBrownfieldConfig[platform], + }; + + return { + ...platformConfig, + ...Object.fromEntries( + Object.entries(options).filter(([, value]) => value !== undefined) + ), + } as T; } diff --git a/packages/cli/src/shared/utils/__tests__/cli.test.ts b/packages/cli/src/shared/utils/__tests__/cli.test.ts index 64c85ef2..c69432e8 100644 --- a/packages/cli/src/shared/utils/__tests__/cli.test.ts +++ b/packages/cli/src/shared/utils/__tests__/cli.test.ts @@ -1,14 +1,9 @@ import * as rockTools from '@rock-js/tools'; -import * as configModule from '../../../config.js'; 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 { @@ -28,7 +23,6 @@ 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'; @@ -51,15 +45,6 @@ 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 6b81992d..850a2ebf 100644 --- a/packages/cli/src/shared/utils/cli.ts +++ b/packages/cli/src/shared/utils/cli.ts @@ -1,7 +1,6 @@ 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) => { @@ -24,10 +23,11 @@ export function curryOptions(programCommand: Command, options: RockCLIOptions) { return programCommand; } -export function actionRunner(fn: (...args: T[]) => Promise) { - return async function wrappedCLIAction(...args: T[]) { +export function actionRunner( + fn: (...args: Args) => Promise +) { + return async function wrappedCLIAction(...args: Args) { 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 bed6e5cb..f6bb7277 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -39,5 +39,8 @@ export type BrownfieldIosConfig = Partial; export type BrownfieldConfig = BrownfieldConfigMetadata & BrownfieldCommonOptions & - BrownfieldAndroidConfig & - BrownfieldIosConfig & { brownie?: BrownieConfig }; + Partial<{ + android: BrownfieldAndroidConfig; + ios: BrownfieldIosConfig; + brownie: BrownieConfig; + }>; From c44c3f73712b302ac614654894e9c472af58f996 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 17 Jun 2026 13:45:37 +0200 Subject: [PATCH 17/20] chore: generate schema.json --- lefthook.yml | 3 + package.json | 1 + packages/cli/package.json | 2 + packages/cli/schema.json | 127 ++++++++++++++++++-------------------- packages/cli/src/types.ts | 11 +++- yarn.lock | 23 ++++++- 6 files changed, 94 insertions(+), 73 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index 5823dad2..3e8e1d20 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,6 +1,9 @@ pre-commit: parallel: true commands: + brownfield-config-schema: + run: yarn generate:schema && git add packages/cli/schema.json + brownfield-navigation-drift: run: node --experimental-strip-types --no-warnings ./scripts/check-brownfield-navigation-drift.ts diff --git a/package.json b/package.json index ca401be9..cb01337d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "brownfield:plugin:release-notes": "node --experimental-strip-types --no-warnings ./scripts/generate-brownfield-gradle-plugin-release-notes.ts", "brownfield:plugin:version:check": "node --experimental-strip-types --no-warnings ./scripts/sync-brownfield-gradle-plugin-version.ts --check", "brownfield:plugin:version:sync": "node --experimental-strip-types --no-warnings ./scripts/sync-brownfield-gradle-plugin-version.ts", + "generate:schema": "yarn workspace @callstack/brownfield-cli generate:schema", "generate:store": "node --experimental-strip-types --no-warnings ./scripts/generate-store.ts", "skillgym:brownie": "skillgym run skillgym/suites/brownie-suite.ts", "skillgym:navigation": "skillgym run skillgym/suites/brownfield-navigation-suite.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 29cf0805..bd823f81 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "build:brownfield": "yarn run build", "build": "node -e \"const fs=require('fs'),p=require('path');const d=p.join('dist','index.d.ts');if(!fs.existsSync(d)){try{fs.unlinkSync('tsconfig.tsbuildinfo')}catch{}}\" && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch", + "generate:schema": "ts-json-schema-generator --path src/types.ts --type BrownfieldConfig --out schema.json && prettier --write schema.json", "test": "vitest run" }, "keywords": [ @@ -106,6 +107,7 @@ "eslint": "^9.39.3", "globals": "^17.3.0", "nodemon": "^3.1.14", + "ts-json-schema-generator": "^2.9.0", "typescript": "5.9.3", "vitest": "^4.1.4" }, diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 175eea7b..00ee5b71 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -1,119 +1,110 @@ { + "$ref": "#/definitions/BrownfieldConfig", "$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" - }, - "android": { - "type": "object", - "description": "Android-specific Brownfield CLI configuration.", + "definitions": { + "BrownfieldAndroidConfig": { "additionalProperties": false, "properties": { "moduleName": { - "type": "string", - "description": "AAR module name." + "type": "string" }, "variant": { - "type": "string", - "description": "Specify your app's build variant, constructed from build type and product flavor, for example \"debug\" or \"freeRelease\"." + "type": "string" } - } + }, + "type": "object" }, - "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.", + "BrownfieldConfig": { "additionalProperties": false, "properties": { - "kotlin": { - "type": "string", - "description": "Directory where generated Kotlin Brownie store files should be written." + "$schema": { + "type": "string" }, - "kotlinPackageName": { - "type": "string", - "description": "Kotlin package name used in generated Brownie store files." + "android": { + "$ref": "#/definitions/BrownfieldAndroidConfig" + }, + "brownie": { + "$ref": "#/definitions/BrownieConfig" + }, + "ios": { + "$ref": "#/definitions/BrownfieldIosConfig" + }, + "verbose": { + "type": "boolean" } - } + }, + "type": "object" }, - "ios": { - "type": "object", - "description": "iOS-specific Brownfield CLI configuration.", + "BrownfieldIosConfig": { "additionalProperties": false, "properties": { + "addSpmPackage": { + "description": "When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs.", + "type": "boolean" + }, "archive": { - "type": "boolean", - "description": "Create an Xcode archive (IPA) of the build, required for uploading to App Store Connect or distributing to TestFlight." + "type": "boolean" }, "buildFolder": { - "type": "string", - "description": "Location for iOS build artifacts. Corresponds to Xcode's \"-derivedDataPath\". By default, the '/.brownfield/build' path is used." + "type": "string" }, "configuration": { - "type": "string", - "description": "Explicitly set the scheme configuration to use. This option is case sensitive." + "type": "string" }, "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" - } + }, + "type": "array" }, "exportExtraParams": { - "type": "array", - "description": "Custom params passed to the xcodebuild export archive command.", "items": { "type": "string" - } + }, + "type": "array" }, "exportOptionsPlist": { - "type": "string", - "description": "Name of the export options file for archiving. Defaults to ExportOptions.plist." + "type": "string" }, "extraParams": { - "type": "array", - "description": "Custom params passed to the xcodebuild command.", "items": { "type": "string" - } + }, + "type": "array" }, "installPods": { - "type": "boolean", - "description": "Whether CocoaPods should be installed automatically. Set to false to match --no-install-pods." + "type": "boolean" }, "local": { - "type": "boolean", - "description": "Force a local build with xcodebuild." + "type": "boolean" }, "newArch": { - "type": "boolean", - "description": "Whether to use the new React Native architecture. Set to false to match --no-new-arch." - }, - "usePrebuiltRnCore": { - "type": "boolean", - "description": "Controls whether iOS packaging uses React Native Apple prebuilt binaries. Omit it to use version-aware defaults for the current React Native or Expo version." + "type": "boolean" }, "scheme": { - "type": "string", - "description": "Explicitly set the Xcode scheme to use." + "type": "string" }, "target": { - "type": "string", - "description": "Explicitly set the Xcode target to use." + "type": "string" }, - "addSpmPackage": { - "type": "boolean", - "description": "Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs." + "usePrebuiltRnCore": { + "description": "Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults).", + "type": "boolean" } - } + }, + "type": "object" }, - "verbose": { - "type": "boolean", - "description": "Enable verbose logging." + "BrownieConfig": { + "additionalProperties": false, + "properties": { + "kotlin": { + "type": "string" + }, + "kotlinPackageName": { + "type": "string" + } + }, + "type": "object" } } } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index f6bb7277..33d04cd6 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -33,9 +33,14 @@ export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial; -export type BrownfieldAndroidConfig = Partial & - Partial; -export type BrownfieldIosConfig = Partial; +export type BrownfieldAndroidConfig = Omit< + Partial & Partial, + keyof BrownfieldCommonOptions +>; +export type BrownfieldIosConfig = Omit< + Partial, + keyof BrownfieldCommonOptions +>; export type BrownfieldConfig = BrownfieldConfigMetadata & BrownfieldCommonOptions & diff --git a/yarn.lock b/yarn.lock index c21e59f1..0071766a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1695,6 +1695,7 @@ __metadata: nodemon: "npm:^3.1.14" quicktype-core: "npm:^23.2.6" quicktype-typescript-input: "npm:^23.2.6" + ts-json-schema-generator: "npm:^2.9.0" ts-morph: "npm:^27.0.2" typescript: "npm:5.9.3" vitest: "npm:^4.1.4" @@ -19426,7 +19427,7 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.2.0, safe-stable-stringify@npm:^2.3.1": +"safe-stable-stringify@npm:^2.2.0, safe-stable-stringify@npm:^2.3.1, safe-stable-stringify@npm:^2.5.0": version: 2.5.0 resolution: "safe-stable-stringify@npm:2.5.0" checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c @@ -20709,6 +20710,24 @@ __metadata: languageName: node linkType: hard +"ts-json-schema-generator@npm:^2.9.0": + version: 2.9.0 + resolution: "ts-json-schema-generator@npm:2.9.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + commander: "npm:^14.0.3" + glob: "npm:^13.0.6" + json5: "npm:^2.2.3" + normalize-path: "npm:^3.0.0" + safe-stable-stringify: "npm:^2.5.0" + tslib: "npm:^2.8.1" + typescript: "npm:^5.9.3" + bin: + ts-json-schema-generator: bin/ts-json-schema-generator.js + checksum: 10/50a18cb6a1171e495af16a4fe8165000d0155ef693cec9345bacb8a5b6cc559dcc730443ebdc025590cd29ffd2019564267a8222c7f6389bdf5558856fbe479b + languageName: node + linkType: hard + "ts-morph@npm:^27.0.2": version: 27.0.2 resolution: "ts-morph@npm:27.0.2" @@ -20769,7 +20788,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.3, tslib@npm:^2.8.0": +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 From e71cfa58a4f353ef1a12c92bbabe1c231f32cb07 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 17 Jun 2026 13:57:05 +0200 Subject: [PATCH 18/20] fix: restore overriding behavior --- packages/cli/src/__tests__/config.test.ts | 32 ++++++++++++++++++++- packages/cli/src/config.ts | 34 ++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index c9315092..a0c5ccb8 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -265,7 +265,37 @@ describe('mergeBrownfieldConfigWithOptions', () => { const mergedOptions = mergeBrownfieldConfigWithOptions(options, 'ios'); expect(mergedOptions.scheme).toBe('CliScheme'); - expect(mockLoggerWarn).not.toHaveBeenCalled(); + expect(mockLoggerWarn).toHaveBeenCalledWith( + 'CLI option "%s" is overriding the react-native-brownfield config value: %s -> %s.', + 'scheme', + 'ConfigScheme', + 'CliScheme' + ); + }); + + it('logs array config values overridden by CLI options', () => { + tempDir = createTempProject({ + packageJsonConfig: { + ios: { + destination: ['simulator'], + }, + }, + }); + process.chdir(tempDir); + + mergeBrownfieldConfigWithOptions( + { + destination: ['device'], + }, + 'ios' + ); + + expect(mockLoggerWarn).toHaveBeenCalledWith( + 'CLI option "%s" is overriding the react-native-brownfield config value: %s -> %s.', + 'destination', + '["simulator"]', + '["device"]' + ); }); it('does not allow undefined options to override platform config', () => { diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index bd92eab8..b5e875e1 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -65,6 +65,14 @@ function getSharedConfig(config: BrownfieldConfig): ConfigurableOptions { return config.verbose === undefined ? {} : { verbose: config.verbose }; } +function formatConfigValue(value: unknown): string { + return typeof value === 'string' ? value : JSON.stringify(value); +} + +function areConfigValuesEqual(configValue: unknown, optionValue: unknown) { + return JSON.stringify(configValue) === JSON.stringify(optionValue); +} + export function mergeBrownfieldConfigWithOptions( options: T, platform: BrownfieldPlatform @@ -73,15 +81,33 @@ export function mergeBrownfieldConfigWithOptions( validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); - const platformConfig = { + const platformConfig: ConfigurableOptions = { ...getSharedConfig(reactNativeBrownfieldConfig), ...reactNativeBrownfieldConfig[platform], }; + const cliOptions = Object.fromEntries( + Object.entries(options).filter(([, value]) => value !== undefined) + ); + + for (const [key, value] of Object.entries(cliOptions)) { + const configValue = platformConfig[key]; + + if ( + configValue !== undefined && + !areConfigValuesEqual(configValue, value) + ) { + logger.warn( + 'CLI option "%s" is overriding the react-native-brownfield config value: %s -> %s.', + key, + formatConfigValue(configValue), + formatConfigValue(value) + ); + } + } + return { ...platformConfig, - ...Object.fromEntries( - Object.entries(options).filter(([, value]) => value !== undefined) - ), + ...cliOptions, } as T; } From 91ebb4ba5fb8e2739834106c28d25a1c3f826b61 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Wed, 17 Jun 2026 14:03:20 +0200 Subject: [PATCH 19/20] fix: set verbose corretly to logger --- packages/cli/src/__tests__/config.test.ts | 23 +++++++++++++++++++++++ packages/cli/src/config.ts | 10 ++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index a0c5ccb8..4b0378e4 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -29,6 +29,9 @@ import { } from '../config.js'; const mockLoggerWarn = rockTools.logger.warn as ReturnType; +const mockLoggerSetVerbose = rockTools.logger.setVerbose as ReturnType< + typeof vi.fn +>; const originalCwd = process.cwd(); function createTempProject({ @@ -336,5 +339,25 @@ describe('mergeBrownfieldConfigWithOptions', () => { verbose: true, moduleName: ':BrownfieldLib', }); + expect(mockLoggerSetVerbose).toHaveBeenCalledWith(true); + }); + + it('applies verbose after CLI options override shared config', () => { + tempDir = createTempProject({ + packageJsonConfig: { + verbose: true, + }, + }); + process.chdir(tempDir); + + const mergedOptions = mergeBrownfieldConfigWithOptions( + { + verbose: false, + }, + 'android' + ); + + expect(mergedOptions.verbose).toBe(false); + expect(mockLoggerSetVerbose).toHaveBeenCalledWith(false); }); }); diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index b5e875e1..b9f4ae12 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -106,8 +106,14 @@ export function mergeBrownfieldConfigWithOptions( } } - return { + const mergedOptions = { ...platformConfig, ...cliOptions, - } as T; + } as T & { verbose?: unknown }; + + if (typeof mergedOptions.verbose === 'boolean') { + logger.setVerbose(mergedOptions.verbose); + } + + return mergedOptions; } From 823d34936b1664cec5a924df6c1b1c683dd01283 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Thu, 18 Jun 2026 14:49:07 +0200 Subject: [PATCH 20/20] chore: add changesets --- .changeset/rich-turkeys-find.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/rich-turkeys-find.md diff --git a/.changeset/rich-turkeys-find.md b/.changeset/rich-turkeys-find.md new file mode 100644 index 00000000..42d79f3d --- /dev/null +++ b/.changeset/rich-turkeys-find.md @@ -0,0 +1,6 @@ +--- +'@callstack/react-native-brownfield': minor +'@callstack/brownfield-cli': minor +--- + +Added configuration file support for react-native-brownfield packages.