diff --git a/.changeset/nine-mice-fall.md b/.changeset/nine-mice-fall.md new file mode 100644 index 00000000..473b5a7f --- /dev/null +++ b/.changeset/nine-mice-fall.md @@ -0,0 +1,6 @@ +--- +'@callstack/react-native-brownfield': patch +'@callstack/brownfield-cli': patch +--- + +feat: support brownfield unified config file in Expo diff --git a/apps/ExpoApp54/app.json b/apps/ExpoApp54/app.json index 3bde2efd..09022af7 100644 --- a/apps/ExpoApp54/app.json +++ b/apps/ExpoApp54/app.json @@ -41,12 +41,7 @@ } } ], - [ - "@callstack/react-native-brownfield", - { - "debug": true - } - ], + "@callstack/react-native-brownfield", "expo-web-browser" ], "experiments": { diff --git a/apps/ExpoApp55/app.json b/apps/ExpoApp55/app.json index 67b61868..22907a5e 100644 --- a/apps/ExpoApp55/app.json +++ b/apps/ExpoApp55/app.json @@ -37,7 +37,7 @@ } } ], - ["@callstack/react-native-brownfield", { "debug": true }], + "@callstack/react-native-brownfield", "expo-image", "expo-font", "expo-web-browser" diff --git a/apps/ExpoApp55/brownfield.config.json b/apps/ExpoApp55/brownfield.config.json new file mode 100644 index 00000000..eee651f9 --- /dev/null +++ b/apps/ExpoApp55/brownfield.config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "android": { + "moduleName": "brownfieldlib" + }, + "ios": { + "scheme": "BrownfieldLib" + }, + "verbose": true, + "brownie": { + "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", + "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp55" + } +} diff --git a/apps/ExpoApp55/package.json b/apps/ExpoApp55/package.json index a18cb58b..dc4ca384 100644 --- a/apps/ExpoApp55/package.json +++ b/apps/ExpoApp55/package.json @@ -65,18 +65,5 @@ "react-test-renderer": "19.2.0", "typescript": "~5.9.2" }, - "private": true, - "brownfield": { - "brownie": { - "kotlin": "./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/", - "kotlinPackageName": "com.callstack.rnbrownfield.demo.expoapp55" - }, - "android": { - "moduleName": "brownfieldlib" - }, - "ios": { - "scheme": "BrownfieldLib" - }, - "verbose": true - } + "private": true } diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx index 5577a06c..6757ca4b 100644 --- a/docs/docs/docs/api-reference/configuration.mdx +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -10,9 +10,9 @@ For example, `--module-name` becomes `moduleName`, `--build-folder` becomes `bui 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 +- `brownfield.config.js` +- `brownfield.config.json` +- `package.json` under the `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. @@ -20,9 +20,68 @@ If the CLI finds multiple sources, it throws an error instead of guessing which 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. +## Expo projects + +For Expo apps, the Brownfield config file is also used by the `@callstack/react-native-brownfield` Expo config plugin during `expo prebuild`. + +Use **one configuration source** for Brownfield options: + +- `brownfield.config.js`, `brownfield.config.json`, or `package.json#brownfield` +- **or** plugin options in the `app.json` `plugins` tuple + +Do not define the same options in both places. If a Brownfield config file exists and `app.json` also passes plugin options, prebuild throws an error. + +### Shared keys between CLI and Expo plugin + +| Config file key | Expo plugin equivalent | Notes | +| -------------------- | ---------------------- | ---------------------------------------------------------- | +| `android.moduleName` | `android.moduleName` | Same key for packaging and prebuild scaffolding. | +| `ios.scheme` | `ios.frameworkName` | Same value; `ios.scheme` is the canonical config file key. | +| `verbose` | `debug` | Same intent; `verbose` is the canonical config file key. | + +### Expo-only keys + +Plugin-only options that do not map to CLI flags belong under nested `expo` objects: + +- `android.expo.*` for Android prebuild scaffolding +- `ios.expo.*` for iOS prebuild scaffolding + +Example: + +```json +{ + "$schema": "https://oss.callstack.com/react-native-brownfield/schema.json", + "verbose": true, + "android": { + "moduleName": "brownfieldlib", + "variant": "release", + "expo": { + "packageName": "com.example.app", + "minSdkVersion": 24 + } + }, + "ios": { + "scheme": "BrownfieldLib", + "configuration": "Release", + "expo": { + "bundleIdentifier": "com.example.app.brownfield", + "deploymentTarget": "15.0" + } + } +} +``` + +For Expo projects, register the plugin without options when using a config file: + +```json +{ + "plugins": ["@callstack/react-native-brownfield"] +} +``` + ## JavaScript config file -If you prefer a JavaScript file, create `react-native-brownfield.config.js` and export a plain object with `module.exports`: +If you prefer a JavaScript file, create `brownfield.config.js` and export a plain object with `module.exports`: ```js /** @type {import('@callstack/react-native-brownfield').BrownfieldConfig} */ @@ -44,7 +103,7 @@ module.exports = { ## JSON config file -If you want schema autocomplete and validation directly in the config file, use `react-native-brownfield.config.json`: +If you want schema autocomplete and validation directly in the config file, use `brownfield.config.json`: > [!TIP] > @@ -110,16 +169,28 @@ All file-based platform options mirror CLI flags, but they use camelCase propert ### Android keys -| 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`. | +| Key | Type | Description | +| -------------------- | -------- | ---------------------------------------------------------------------------------- | +| `android.moduleName` | `string` | Android module name used for packaging, publishing, and Expo prebuild scaffolding. | +| `android.variant` | `string` | Android build variant, for example `debug` or `freeRelease`. | + +#### Android Expo keys + +| Key | Type | Description | +| -------------------------------- | -------- | ------------------------------------------------------ | +| `android.expo.packageName` | `string` | Package name for the generated Android library module. | +| `android.expo.minSdkVersion` | `number` | Minimum Android SDK supported by the library. | +| `android.expo.targetSdkVersion` | `number` | Target Android SDK version for the library. | +| `android.expo.compileSdkVersion` | `number` | Compile Android SDK version used to build the library. | +| `android.expo.groupId` | `string` | Maven group ID used when publishing the AAR. | +| `android.expo.artifactId` | `string` | Maven artifact ID used when publishing the AAR. | +| `android.expo.version` | `string` | Maven version used when publishing the AAR. | ### iOS keys | Key | Type | Description | | ------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------- | -| `ios.scheme` | `string` | Xcode scheme used for packaging. | +| `ios.scheme` | `string` | Xcode scheme used for packaging. Also used as the generated framework name during Expo prebuild. | | `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. | @@ -132,6 +203,15 @@ All file-based platform options mirror CLI flags, but they use camelCase propert | `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. | +#### iOS Expo keys + +| Key | Type | Description | +| --------------------------- | -------- | --------------------------------------------------------------- | +| `ios.expo.bundleIdentifier` | `string` | Bundle identifier assigned to the generated framework. | +| `ios.expo.buildSettings` | `object` | Additional Xcode build settings applied to the framework build. | +| `ios.expo.deploymentTarget` | `string` | Minimum iOS version supported by the generated framework. | +| `ios.expo.frameworkVersion` | `string` | Framework version used for Apple build settings. | + ## Brownie configuration The Brownie configuration lives inside the main Brownfield config under the `brownie` key. @@ -144,7 +224,7 @@ Currently supported Brownie keys are: | `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`: +Example inside `brownfield.config.json`: ```json { diff --git a/docs/docs/docs/getting-started/android.mdx b/docs/docs/docs/getting-started/android.mdx index 5ce0c33f..0f55091c 100644 --- a/docs/docs/docs/getting-started/android.mdx +++ b/docs/docs/docs/getting-started/android.mdx @@ -334,7 +334,7 @@ tasks.named("generateMetadataFileForMavenAarPublication") { ## 7. Create a Brownfield Configuration -Create `react-native-brownfield.config.json` in your project root: +Create `brownfield.config.json` in your project root: ```json { diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index 3045029a..e7c7c8fc 100644 --- a/docs/docs/docs/getting-started/expo.mdx +++ b/docs/docs/docs/getting-started/expo.mdx @@ -176,7 +176,9 @@ ReactNativeView(moduleName: "ExpoRNApp") ## Plugin Options -You can pass plugin options through the second item in the `plugins` tuple in `app.json`: +For Expo projects, prefer a [`brownfield.config.json`](/docs/api-reference/configuration#expo-projects) file so the same settings drive both `expo prebuild` and Brownfield CLI commands. + +You can still pass plugin options through the second item in the `plugins` tuple in `app.json` when you do **not** use a Brownfield config file: ```json { @@ -186,13 +188,11 @@ You can pass plugin options through the second item in the `plugins` tuple in `a { "ios": { "frameworkName": "MyBrownfieldLib", - "bundleIdentifier": "com.example.app.brownfield", - ... + "bundleIdentifier": "com.example.app.brownfield" }, "android": { "moduleName": "mybrownfieldlib", - "packageName": "com.example.mybrownfieldlib", - ... + "packageName": "com.example.mybrownfieldlib" } } ] @@ -200,10 +200,22 @@ You can pass plugin options through the second item in the `plugins` tuple in `a } ``` +:::warning +If you use `brownfield.config.js`, `brownfield.config.json`, or `package.json#brownfield`, do not pass plugin options in `app.json`. Prebuild throws an error when both sources are used. +::: + +When using a Brownfield config file, register the plugin without options: + +```json +{ + "plugins": ["@callstack/react-native-brownfield"] +} +``` + ### iOS - `frameworkName` (`string`, default: `"BrownfieldLib"`) - - Name of the generated framework. This is also used as the XCFramework name. + - Name of the generated framework. This is also used as the XCFramework name. In `brownfield.config.*`, use `ios.scheme` instead. - `bundleIdentifier` (`string`, default: app bundle identifier + `.brownfield`) - Bundle identifier assigned to the generated framework. - `buildSettings` (`Record`, optional) diff --git a/docs/docs/docs/getting-started/ios.mdx b/docs/docs/docs/getting-started/ios.mdx index 9ac0cf5b..a77cea21 100644 --- a/docs/docs/docs/getting-started/ios.mdx +++ b/docs/docs/docs/getting-started/ios.mdx @@ -107,7 +107,7 @@ class InternalClassForBundle {} ## 5. Create a Brownfield Configuration -Create `react-native-brownfield.config.json` in your project root: +Create `brownfield.config.json` in your project root: ```json { diff --git a/packages/cli/package.json b/packages/cli/package.json index a53fa2cd..ded2a1b0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,11 @@ "types": "./dist/types.d.ts", "default": "./dist/types.js" }, + "./expo-plugin-config": { + "source": "./src/expoPluginConfig.ts", + "types": "./dist/expoPluginConfig.d.ts", + "default": "./dist/expoPluginConfig.js" + }, "./package.json": "./package.json" }, "scripts": { diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 25e22adc..c2e5b0c2 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -5,6 +5,10 @@ "BrownfieldAndroidConfig": { "additionalProperties": false, "properties": { + "expo": { + "$ref": "#/definitions/BrownfieldExpoAndroidConfig", + "description": "Expo config plugin options for Android Expo prebuild phase." + }, "moduleName": { "description": "AAR module name.", "type": "string" @@ -41,6 +45,67 @@ }, "type": "object" }, + "BrownfieldExpoAndroidConfig": { + "additionalProperties": false, + "description": "Expo config plugin options for Android prebuild scaffolding.", + "properties": { + "artifactId": { + "description": "Maven artifact ID used when publishing the AAR.", + "type": "string" + }, + "compileSdkVersion": { + "description": "Compile SDK version for the Android library.", + "type": "number" + }, + "groupId": { + "description": "Maven group ID used when publishing the AAR.", + "type": "string" + }, + "minSdkVersion": { + "description": "Minimum SDK version for the Android library.", + "type": "number" + }, + "packageName": { + "description": "The package name for the Android library module.", + "type": "string" + }, + "targetSdkVersion": { + "description": "Target SDK version for the Android library.", + "type": "number" + }, + "version": { + "description": "Maven version used when publishing the AAR.", + "type": "string" + } + }, + "type": "object" + }, + "BrownfieldExpoIosConfig": { + "additionalProperties": false, + "description": "Expo config plugin options for iOS prebuild scaffolding.", + "properties": { + "buildSettings": { + "additionalProperties": { + "type": ["string", "boolean", "number"] + }, + "description": "Custom build settings applied when building the framework.", + "type": "object" + }, + "bundleIdentifier": { + "description": "The bundle identifier for the framework.", + "type": "string" + }, + "deploymentTarget": { + "description": "Minimum iOS deployment target for the generated framework.", + "type": "string" + }, + "frameworkVersion": { + "description": "Framework version used for Apple build settings.", + "type": "string" + } + }, + "type": "object" + }, "BrownfieldIosConfig": { "additionalProperties": false, "properties": { @@ -63,6 +128,10 @@ }, "type": "array" }, + "expo": { + "$ref": "#/definitions/BrownfieldExpoIosConfig", + "description": "Expo config plugin options for iOS Expo prebuild phase." + }, "exportExtraParams": { "description": "Custom xcodebuild export archive parameters.", "items": { diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index 4b0378e4..872ecfd0 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -39,7 +39,7 @@ function createTempProject({ jsConfig, jsonConfig, }: { - packageJsonConfig?: Record; + packageJsonConfig?: Record | null; jsConfig?: Record; jsonConfig?: Record; } = {}): string { @@ -146,9 +146,25 @@ describe('loadBrownfieldConfig', () => { }); }); - it('returns an empty config when no source exists', () => { + it('returns null when no source exists', () => { tempDir = createTempProject(); + expect(loadBrownfieldConfig(tempDir)).toBeNull(); + }); + + it('returns an empty config when package.json brownfield key is empty', () => { + tempDir = createTempProject({ + packageJsonConfig: {}, + }); + + expect(loadBrownfieldConfig(tempDir)).toEqual({}); + }); + + it('returns an empty config when package.json brownfield key is null', () => { + tempDir = createTempProject({ + packageJsonConfig: null, + }); + expect(loadBrownfieldConfig(tempDir)).toEqual({}); }); @@ -170,6 +186,17 @@ describe('loadBrownfieldConfig', () => { 'Project has multiple Brownfield configuration files' ); }); + + it('throws when package.json brownfield key is null and a config file exists', () => { + tempDir = createTempProject({ + packageJsonConfig: null, + jsonConfig: { verbose: true }, + }); + + expect(() => loadBrownfieldConfig(tempDir!)).toThrow( + 'Project has multiple Brownfield configuration files' + ); + }); }); describe('validateBrownfieldCLIConfig', () => { diff --git a/packages/cli/src/__tests__/expoPluginConfig.test.ts b/packages/cli/src/__tests__/expoPluginConfig.test.ts new file mode 100644 index 00000000..8740bf63 --- /dev/null +++ b/packages/cli/src/__tests__/expoPluginConfig.test.ts @@ -0,0 +1,318 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../brownfield/utils/paths.js', () => ({ + findProjectRoot: vi.fn(() => process.cwd()), +})); + +import { loadBrownfieldConfig } from '../config.js'; +import { + assertNoConfigFilePluginOverlap, + resolveBrownfieldPluginConfig, +} from '../expoPluginConfig.js'; + +const originalCwd = process.cwd(); + +function createTempProject({ + packageJsonConfig, + jsConfig, + jsonConfig, +}: { + packageJsonConfig?: Record; + jsConfig?: Record; + jsonConfig?: Record; +} = {}): string { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'brownfield-expo-config-') + ); + + const packageJson: Record = { + name: 'temp-project', + version: '1.0.0', + }; + + if (packageJsonConfig !== undefined) { + packageJson['brownfield'] = packageJsonConfig; + } + + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify(packageJson, null, 2) + ); + + if (jsConfig !== undefined) { + fs.writeFileSync( + path.join(tempDir, 'brownfield.config.js'), + `module.exports = ${JSON.stringify(jsConfig, null, 2)};\n` + ); + } + + if (jsonConfig !== undefined) { + fs.writeFileSync( + path.join(tempDir, 'brownfield.config.json'), + JSON.stringify(jsonConfig, null, 2) + ); + } + + return tempDir; +} + +const baseExpoConfig = { + ios: { + bundleIdentifier: 'com.example.app', + }, + android: { + package: 'com.example.app', + }, +}; + +describe('loadBrownfieldConfig', () => { + let tempDir: string | null = null; + + afterEach(() => { + if (tempDir) { + process.chdir(originalCwd); + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it('returns config when brownfield.config.json exists', () => { + tempDir = createTempProject({ + jsonConfig: { verbose: true }, + }); + process.chdir(tempDir); + + expect(loadBrownfieldConfig()).toEqual({ verbose: true }); + }); + + it('returns config when package.json contains a brownfield key', () => { + tempDir = createTempProject({ + packageJsonConfig: {}, + }); + process.chdir(tempDir); + + expect(loadBrownfieldConfig()).toEqual({}); + }); + + it('returns null when no brownfield config source exists', () => { + tempDir = createTempProject(); + process.chdir(tempDir); + + expect(loadBrownfieldConfig()).toBeNull(); + }); +}); + +describe('assertNoConfigFilePluginOverlap', () => { + let tempDir: string | null = null; + + afterEach(() => { + if (tempDir) { + process.chdir(originalCwd); + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it('allows empty plugin props when a config file exists', () => { + tempDir = createTempProject({ + jsonConfig: { android: { moduleName: 'brownfieldlib' } }, + }); + process.chdir(tempDir); + + expect(() => + assertNoConfigFilePluginOverlap( + { android: { moduleName: 'brownfieldlib' } }, + {} + ) + ).not.toThrow(); + }); + + it('throws when a config file exists and plugin props are non-empty', () => { + tempDir = createTempProject({ + jsonConfig: { android: { moduleName: 'brownfieldlib' } }, + }); + process.chdir(tempDir); + + expect(() => + assertNoConfigFilePluginOverlap( + { android: { moduleName: 'brownfieldlib' } }, + { android: { moduleName: 'brownfieldlib' } } + ) + ).toThrow(/both a brownfield config file and app.json plugin options/); + }); + + it('throws when a config file exists and debug is set in plugin props', () => { + tempDir = createTempProject({ + jsonConfig: { verbose: true }, + }); + process.chdir(tempDir); + + expect(() => + assertNoConfigFilePluginOverlap({ verbose: true }, { debug: true }) + ).toThrow(/both a brownfield config file and app.json plugin options/); + }); + + it('allows non-empty plugin props when no config file exists', () => { + tempDir = createTempProject(); + process.chdir(tempDir); + + expect(() => + assertNoConfigFilePluginOverlap(null, { + ios: { frameworkName: 'BrownfieldLib' }, + }) + ).not.toThrow(); + }); +}); + +describe('resolveBrownfieldPluginConfig', () => { + let tempDir: string | null = null; + + beforeEach(() => { + tempDir = createTempProject(); + process.chdir(tempDir); + }); + + afterEach(() => { + if (tempDir) { + process.chdir(originalCwd); + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it('resolves defaults from Expo config when no file config exists', () => { + const resolved = resolveBrownfieldPluginConfig({}, null, baseExpoConfig); + + expect(resolved).toEqual({ + debug: false, + ios: { + frameworkName: 'BrownfieldLib', + bundleIdentifier: 'com.example.app.brownfield', + buildSettings: {}, + deploymentTarget: '15.0', + frameworkVersion: '1', + }, + android: { + moduleName: 'brownfieldlib', + packageName: 'com.example.app', + minSdkVersion: 24, + targetSdkVersion: 35, + compileSdkVersion: 35, + groupId: 'com.example.app', + artifactId: 'brownfieldlib', + version: '0.0.1-SNAPSHOT', + }, + }); + }); + + it('uses legacy app.json plugin props when no config file exists', () => { + const resolved = resolveBrownfieldPluginConfig( + { + debug: true, + ios: { frameworkName: 'CustomLib' }, + android: { moduleName: 'customlib' }, + }, + null, + baseExpoConfig + ); + + expect(resolved.debug).toBe(true); + expect(resolved.ios?.frameworkName).toBe('CustomLib'); + expect(resolved.android?.moduleName).toBe('customlib'); + }); + + it('strips leading ":" from android.moduleName when resolving from file config', () => { + const resolved = resolveBrownfieldPluginConfig( + {}, + { android: { moduleName: ':mylib' } }, + baseExpoConfig + ); + expect(resolved.android?.moduleName).toBe('mylib'); + expect(resolved.android?.artifactId).toBe('mylib'); + }); + + it('maps ios.scheme to frameworkName and verbose to debug from file config', () => { + fs.writeFileSync( + path.join(tempDir!, 'brownfield.config.json'), + JSON.stringify({ + verbose: true, + android: { moduleName: 'mylib' }, + ios: { scheme: 'MyLib' }, + }) + ); + + const resolved = resolveBrownfieldPluginConfig( + {}, + { + verbose: true, + android: { moduleName: 'mylib' }, + ios: { scheme: 'MyLib' }, + }, + baseExpoConfig + ); + + expect(resolved.debug).toBe(true); + expect(resolved.ios?.frameworkName).toBe('MyLib'); + expect(resolved.android?.moduleName).toBe('mylib'); + }); + + it('merges android.expo and ios.expo from file config', () => { + fs.writeFileSync( + path.join(tempDir!, 'brownfield.config.json'), + JSON.stringify({ + android: { + moduleName: 'mylib', + expo: { + minSdkVersion: 26, + version: '2.0.0', + }, + }, + ios: { + scheme: 'MyLib', + expo: { + deploymentTarget: '16.0', + bundleIdentifier: 'com.example.framework', + }, + }, + }) + ); + + const fileConfig = { + android: { + moduleName: 'mylib', + expo: { + minSdkVersion: 26, + version: '2.0.0', + }, + }, + ios: { + scheme: 'MyLib', + expo: { + deploymentTarget: '16.0', + bundleIdentifier: 'com.example.framework', + }, + }, + }; + + const resolved = resolveBrownfieldPluginConfig( + {}, + fileConfig, + baseExpoConfig + ); + + expect(resolved.android).toMatchObject({ + moduleName: 'mylib', + minSdkVersion: 26, + version: '2.0.0', + }); + expect(resolved.ios).toMatchObject({ + frameworkName: 'MyLib', + deploymentTarget: '16.0', + bundleIdentifier: 'com.example.framework', + }); + }); +}); diff --git a/packages/cli/src/brownfield/commands/__tests__/packageIos.action.test.ts b/packages/cli/src/brownfield/commands/__tests__/packageIos.action.test.ts index a9751cf7..a5f79990 100644 --- a/packages/cli/src/brownfield/commands/__tests__/packageIos.action.test.ts +++ b/packages/cli/src/brownfield/commands/__tests__/packageIos.action.test.ts @@ -9,10 +9,7 @@ import { runNavigationCodegenIfApplicable } from '../../../navigation/helpers/ru import { packageIosCommand } from '../packageIos.js'; import { copyDebugBundleToSimulatorSlice } from '../../utils/copyDebugBundleToSimulatorSlice.js'; import { createLocalSpmPackage } from '../../utils/createLocalSpmPackage.js'; -import { runExpoPrebuildIfNeeded } from '../../utils/expo.js'; -import { getProjectInfo } from '../../utils/project.js'; import { resolvePackagedFrameworkName } from '../../utils/resolvePackagedFrameworkName.js'; -import { supportsPrebuiltRNCore } from '../../utils/supportsPrebuiltRNCore.js'; vi.mock('@rock-js/platform-apple-helpers', async (importOriginal) => { const actual = await importOriginal(); @@ -54,10 +51,18 @@ vi.mock('@rock-js/tools', async (importOriginal) => { }; }); +vi.mock('../../../config.js', () => ({ + mergeBrownfieldConfigWithOptions: vi.fn((options) => options), +})); + vi.mock('../../utils/expo.js', () => ({ runExpoPrebuildIfNeeded: vi.fn(), })); +vi.mock('../../utils/paths.js', () => ({ + findProjectRoot: vi.fn(() => '/repo'), +})); + vi.mock('../../utils/project.js', () => ({ getProjectInfo: vi.fn(() => ({ projectRoot: '/repo', @@ -90,11 +95,14 @@ vi.mock('../../../brownie/helpers/runBrownieCodegenIfApplicable.js', () => ({ })), })); -vi.mock('../../../navigation/helpers/runNavigationCodegenIfApplicable.js', () => ({ - runNavigationCodegenIfApplicable: vi.fn(async () => ({ - hasNavigation: false, - })), -})); +vi.mock( + '../../../navigation/helpers/runNavigationCodegenIfApplicable.js', + () => ({ + runNavigationCodegenIfApplicable: vi.fn(async () => ({ + hasNavigation: false, + })), + }) +); vi.mock('../../utils/copyDebugBundleToSimulatorSlice.js', () => ({ copyDebugBundleToSimulatorSlice: vi.fn(), @@ -145,7 +153,11 @@ describe('package:ios action --add-spm-package', () => { }); test('calls createLocalSpmPackage with the resolved framework name', async () => { - await invokePackageIosAction(['--add-spm-package', '--configuration', 'Release']); + await invokePackageIosAction([ + '--add-spm-package', + '--configuration', + 'Release', + ]); expect(packageIosAction).toHaveBeenCalledOnce(); expect(copyDebugBundleToSimulatorSlice).toHaveBeenCalledWith({ @@ -172,7 +184,11 @@ describe('package:ios action --add-spm-package', () => { candidates: [], }); - await invokePackageIosAction(['--add-spm-package', '--configuration', 'Release']); + await invokePackageIosAction([ + '--add-spm-package', + '--configuration', + 'Release', + ]); expect(mockCreateLocalSpmPackage).not.toHaveBeenCalled(); expect(mockLoggerWarn).not.toHaveBeenCalled(); @@ -189,7 +205,11 @@ describe('package:ios action --add-spm-package', () => { candidates: ['AppOne', 'AppTwo'], }); - await invokePackageIosAction(['--add-spm-package', '--configuration', 'Debug']); + await invokePackageIosAction([ + '--add-spm-package', + '--configuration', + 'Debug', + ]); expect(mockCreateLocalSpmPackage).not.toHaveBeenCalled(); expect(mockLoggerWarn).not.toHaveBeenCalled(); diff --git a/packages/cli/src/brownfield/commands/packageAndroid.ts b/packages/cli/src/brownfield/commands/packageAndroid.ts index abd69b07..a24031f6 100644 --- a/packages/cli/src/brownfield/commands/packageAndroid.ts +++ b/packages/cli/src/brownfield/commands/packageAndroid.ts @@ -12,6 +12,7 @@ import { curryOptions, } from '../../shared/index.js'; import { runExpoPrebuildIfNeeded } from '../utils/expo.js'; +import { findProjectRoot } from '../utils/paths.js'; import { getProjectInfo } from '../utils/project.js'; import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js'; import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js'; @@ -23,13 +24,15 @@ export const packageAndroidCommand = curryOptions( ).action( actionRunner(async (cliOptions: PackageAarFlags) => { const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android'); + const projectRoot = findProjectRoot(); - const { projectRoot, platformConfig } = getProjectInfo('android'); await runExpoPrebuildIfNeeded({ projectRoot, platform: 'android', }); + const { platformConfig } = getProjectInfo('android'); + await runBrownieCodegenIfApplicable(projectRoot, 'kotlin'); await runNavigationCodegenIfApplicable(projectRoot); diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts index cde328d7..f42f591c 100644 --- a/packages/cli/src/brownfield/commands/packageIos.ts +++ b/packages/cli/src/brownfield/commands/packageIos.ts @@ -17,6 +17,7 @@ import { import { Command, Option } from 'commander'; import { runExpoPrebuildIfNeeded } from '../utils/expo.js'; +import { findProjectRoot } from '../utils/paths.js'; import { getProjectInfo } from '../utils/project.js'; import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js'; import { @@ -97,8 +98,11 @@ export const packageIosCommand = curryOptions( .action( actionRunner(async (cliOptions: PackageIosOptions) => { const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios'); + const projectRoot = findProjectRoot(); - const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios'); + await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' }); + + const { platformConfig, userConfig } = getProjectInfo('ios'); const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot }); @@ -126,8 +130,6 @@ export const packageIosCommand = curryOptions( throw new RockError(prebuiltRNCoreSupport.reason); } - await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' }); - if (!userConfig.project.ios) { throw new Error('iOS project not found.'); } diff --git a/packages/cli/src/brownfield/commands/publishAndroid.ts b/packages/cli/src/brownfield/commands/publishAndroid.ts index dee9d60e..f64bd2eb 100644 --- a/packages/cli/src/brownfield/commands/publishAndroid.ts +++ b/packages/cli/src/brownfield/commands/publishAndroid.ts @@ -12,6 +12,7 @@ import { ExampleUsage, } from '../../shared/index.js'; import { getProjectInfo } from '../utils/project.js'; +import { findProjectRoot } from '../utils/paths.js'; import { runExpoPrebuildIfNeeded } from '../utils/expo.js'; import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js'; import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js'; @@ -25,13 +26,15 @@ export const publishAndroidCommand = curryOptions( ).action( actionRunner(async (cliOptions: PublishLocalAarFlags) => { const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android'); + const projectRoot = findProjectRoot(); - const { projectRoot, platformConfig } = getProjectInfo('android'); await runExpoPrebuildIfNeeded({ projectRoot, platform: 'android', }); + const { platformConfig } = getProjectInfo('android'); + await runBrownieCodegenIfApplicable(projectRoot, 'kotlin'); await runNavigationCodegenIfApplicable(projectRoot); diff --git a/packages/cli/src/brownfield/utils/project.ts b/packages/cli/src/brownfield/utils/project.ts index af3aea22..5b57f2f3 100644 --- a/packages/cli/src/brownfield/utils/project.ts +++ b/packages/cli/src/brownfield/utils/project.ts @@ -21,15 +21,46 @@ const cliConfig: typeof cliConfigImport = : // @ts-expect-error: interop default cliConfigImport.default; +function hasExpoAppConfig(projectRoot: string): boolean { + return ( + fs.existsSync(path.join(projectRoot, 'app.json')) || + fs.existsSync(path.join(projectRoot, 'app.config.js')) || + fs.existsSync(path.join(projectRoot, 'app.config.ts')) || + fs.existsSync(path.join(projectRoot, 'app.config.mjs')) + ); +} + +function projectDependsOnExpo(projectRoot: string): boolean { + const packageJsonPath = path.join(projectRoot, 'package.json'); + + if (!fs.existsSync(packageJsonPath)) { + return false; + } + + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) as Record>; + + return ['dependencies', 'peerDependencies', 'devDependencies'].some( + (key) => packageJson[key]?.expo + ); +} + /** * Gets the Expo config if the project is an Expo project * @param projectRoot The project root path * @returns The Expo config if the project is an Expo project, null otherwise */ export function getExpoConfigIfIsExpo(projectRoot: string) { + const hasAppConfig = hasExpoAppConfig(projectRoot); + try { return getConfig(projectRoot, { skipSDKVersionRequirement: true }); - } catch { + } catch (error) { + if (hasAppConfig) { + throw error; + } + return null; } } @@ -41,19 +72,7 @@ export function getExpoConfigIfIsExpo(projectRoot: string) { * @returns Whether the project is an Expo project */ export function isExpoProject(projectRoot: string): boolean { - const hasExpoConfig = getExpoConfigIfIsExpo(projectRoot) !== null; - - // additionally, it is needed to check if the project depends on Expo packages explicitly - // to prevent false positives in a monorepo setup - const rnProjectRoot = findProjectRoot(); - const packageJsonPath = path.join(rnProjectRoot, 'package.json'); - const dependsOnExpo = - fs.existsSync(packageJsonPath) && - ['dependencies', 'peerDependencies', 'devDependencies'].some( - (key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo - ); - - return hasExpoConfig && dependsOnExpo; + return hasExpoAppConfig(projectRoot) && projectDependsOnExpo(projectRoot); } export function getExpoSdkMajor(projectRoot: string): number | null { diff --git a/packages/cli/src/brownie/__tests__/config.test.ts b/packages/cli/src/brownie/__tests__/config.test.ts index 32263c0e..8565e989 100644 --- a/packages/cli/src/brownie/__tests__/config.test.ts +++ b/packages/cli/src/brownie/__tests__/config.test.ts @@ -40,8 +40,9 @@ describe('loadConfig', () => { }); it('throws when package.json not found', () => { - mockCwd.mockReturnValue('/nonexistent/path'); - expect(() => loadConfig()).toThrow('package.json not found'); + expect(() => loadConfig('/nonexistent/path')).toThrow( + 'package.json not found' + ); }); it('returns empty config when brownie config missing', () => { diff --git a/packages/cli/src/brownie/config.ts b/packages/cli/src/brownie/config.ts index 1224fcb8..59e6602a 100644 --- a/packages/cli/src/brownie/config.ts +++ b/packages/cli/src/brownie/config.ts @@ -108,7 +108,7 @@ export function resolveBrownieCodegenConfig({ const legacyConfig = hasLegacyConfig(projectRoot) ? loadConfig(projectRoot) : undefined; - const brownfieldBrownie = loadBrownfieldConfig(projectRoot).brownie; + const brownfieldBrownie = loadBrownfieldConfig(projectRoot)?.brownie; if (legacyConfig !== undefined && (brownie ?? brownfieldBrownie)) { throw new Error(LEGACY_AND_NEW_BROWNIE_CONFIG_ERROR); diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index b9f4ae12..ce6ac422 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -10,13 +10,13 @@ import { findProjectRoot } from './brownfield/utils/paths.js'; import BrownfieldSchema from '../schema.json' with { type: 'json' }; import { logger } from '@rock-js/tools'; -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`; +export const CONFIG_BASE_NAME = 'brownfield'; +export const JS_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.js`; +export const JSON_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.json`; const SEPARATOR = '\nā— '; -const ajv = new Ajv({ allErrors: true }); +const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); const validateBrownfieldConfig = ajv.compile(BrownfieldSchema); export function validateBrownfieldCLIConfig(config: unknown): void { @@ -29,7 +29,7 @@ export function validateBrownfieldCLIConfig(config: unknown): void { export function loadBrownfieldConfig( projectRoot: string = findProjectRoot() -): BrownfieldConfig { +): BrownfieldConfig | null { const require = createRequire(path.join(projectRoot, 'package.json')); const jsConfigFilePath = path.join(projectRoot, JS_CONFIG_FILE_NAME); @@ -41,7 +41,7 @@ export function loadBrownfieldConfig( [ fs.existsSync(jsConfigFilePath), fs.existsSync(jsonConfigFilePath), - packageJson[CONFIG_BASE_NAME], + CONFIG_BASE_NAME in packageJson, ].filter(Boolean).length > 1 ) { throw new Error('Project has multiple Brownfield configuration files'); @@ -55,7 +55,11 @@ export function loadBrownfieldConfig( return require(jsonConfigFilePath) as BrownfieldConfig; } - return packageJson[CONFIG_BASE_NAME] || {}; + if (CONFIG_BASE_NAME in packageJson) { + return (packageJson[CONFIG_BASE_NAME] as BrownfieldConfig) ?? {}; + } + + return null; } type BrownfieldPlatform = 'android' | 'ios'; @@ -77,7 +81,7 @@ export function mergeBrownfieldConfigWithOptions( options: T, platform: BrownfieldPlatform ): T { - const reactNativeBrownfieldConfig = loadBrownfieldConfig(); + const reactNativeBrownfieldConfig = loadBrownfieldConfig() ?? {}; validateBrownfieldCLIConfig(reactNativeBrownfieldConfig); diff --git a/packages/cli/src/expoPluginConfig.ts b/packages/cli/src/expoPluginConfig.ts new file mode 100644 index 00000000..830da182 --- /dev/null +++ b/packages/cli/src/expoPluginConfig.ts @@ -0,0 +1,191 @@ +import type { BrownfieldConfig } from './types.js'; + +export { loadBrownfieldConfig } from './config.js'; + +/** + * Legacy Expo config plugin props shape accepted via app.json plugins tuple. + */ +export type BrownfieldPluginProps = { + debug?: boolean; + ios?: { + frameworkName?: string; + bundleIdentifier?: string; + buildSettings?: Record; + deploymentTarget?: string; + frameworkVersion?: string; + }; + android?: { + moduleName?: string; + packageName?: string; + minSdkVersion?: number; + targetSdkVersion?: number; + compileSdkVersion?: number; + groupId?: string; + artifactId?: string; + version?: string; + }; +}; + +export type ResolvedBrownfieldPluginAndroidConfig = { + moduleName: string; + packageName: string; + minSdkVersion: number; + targetSdkVersion: number; + compileSdkVersion: number; + groupId: string; + artifactId: string; + version: string; +}; + +export type ResolvedBrownfieldPluginIosConfig = { + frameworkName: string; + bundleIdentifier: string; + buildSettings: Record; + deploymentTarget: string; + frameworkVersion: string; +}; + +export type ResolvedBrownfieldPluginConfig = { + debug: boolean; + ios: ResolvedBrownfieldPluginIosConfig | null; + android: ResolvedBrownfieldPluginAndroidConfig | null; +}; + +type BrownfieldExpoConfig = { + ios?: { + bundleIdentifier?: string; + }; + android?: { + package?: string; + }; +}; + +const CONFIG_FILE_PLUGIN_OVERLAP_ERROR = + 'Brownfield configuration is defined in both a brownfield config file and app.json plugin options. ' + + 'Use only one source: either brownfield.config.js, brownfield.config.json, package.json#brownfield, or the plugin options in app.json.'; + +/** + * Checks if the plugin props are non-empty. + * @param props - The plugin props to check. + * @returns True if the plugin props are non-empty, false otherwise. + */ +function isPluginPropsNonEmpty(props: BrownfieldPluginProps): boolean { + if (props.debug !== undefined) { + return true; + } + + if (props.ios && Object.keys(props.ios).length > 0) { + return true; + } + + if (props.android && Object.keys(props.android).length > 0) { + return true; + } + + return false; +} + +/** + * Converts the file config to plugin props. + * @param fileConfig - The file config to convert to plugin props. + * @returns The plugin props. + */ +function fileConfigToPluginProps( + fileConfig: BrownfieldConfig +): BrownfieldPluginProps { + const props: BrownfieldPluginProps = {}; + + if (fileConfig.verbose !== undefined) { + props.debug = fileConfig.verbose; + } + + if (fileConfig.android) { + props.android = { + moduleName: fileConfig.android.moduleName, + ...fileConfig.android.expo, + }; + } + + if (fileConfig.ios) { + props.ios = { + frameworkName: fileConfig.ios.scheme, + ...fileConfig.ios.expo, + }; + } + + return props; +} + +/** + * Asserts that there is no overlap between the file config and the plugin props. + * @param fileConfig - The loaded file config, or null when no config source exists. + * @param pluginProps - The plugin props to check for overlap. + * @throws An error if there is overlap. + * @returns void + */ +export function assertNoConfigFilePluginOverlap( + fileConfig: BrownfieldConfig | null, + pluginProps: BrownfieldPluginProps +): void { + if (fileConfig === null) { + return; + } + + if (isPluginPropsNonEmpty(pluginProps)) { + throw new Error(CONFIG_FILE_PLUGIN_OVERLAP_ERROR); + } +} + +/** + * Resolves the plugin config from the file config and the plugin props. + * @param pluginProps - The plugin props to resolve. + * @param fileConfig - The file config to resolve. + * @param expoConfig - The Expo plugin config to resolve. + * @returns The resolved plugin config. + */ +export function resolveBrownfieldPluginConfig( + pluginProps: BrownfieldPluginProps, + fileConfig: BrownfieldConfig | null, + expoConfig: BrownfieldExpoConfig +): ResolvedBrownfieldPluginConfig { + const effectiveProps = + fileConfig !== null ? fileConfigToPluginProps(fileConfig) : pluginProps; + + const androidPackage = expoConfig.android?.package; + /** + * Below: android.moduleName may be provided in the fully-qualified Gradle format + * (e.g. :BrownfieldLib — this is shown in the CLI example usage). + * The Expo prebuild side expects a raw module folder/name (it later + * prepends : itself and uses it as a path), so a leading : is removed. + */ + const androidModuleName = ( + effectiveProps.android?.moduleName ?? 'brownfieldlib' + ).replace(/^:/, ''); + + return { + debug: effectiveProps.debug ?? false, + ios: expoConfig.ios + ? { + frameworkName: effectiveProps.ios?.frameworkName ?? 'BrownfieldLib', + bundleIdentifier: + effectiveProps.ios?.bundleIdentifier ?? + `${expoConfig.ios.bundleIdentifier}.brownfield`, + buildSettings: effectiveProps.ios?.buildSettings ?? {}, + deploymentTarget: effectiveProps.ios?.deploymentTarget ?? '15.0', + frameworkVersion: effectiveProps.ios?.frameworkVersion ?? '1', + } + : null, + android: androidPackage + ? { + moduleName: androidModuleName, + packageName: effectiveProps.android?.packageName ?? androidPackage, + minSdkVersion: effectiveProps.android?.minSdkVersion ?? 24, + targetSdkVersion: effectiveProps.android?.targetSdkVersion ?? 35, + compileSdkVersion: effectiveProps.android?.compileSdkVersion ?? 35, + groupId: effectiveProps.android?.groupId ?? androidPackage, + artifactId: effectiveProps.android?.artifactId ?? androidModuleName, + version: effectiveProps.android?.version ?? '0.0.1-SNAPSHOT', + } + : null, + }; +} diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index fa316512..42fe6231 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -44,14 +44,90 @@ export type BrownfieldPublishAndroidOptions = BrownfieldCommonOptions & export type BrownfieldPackageIosOptions = BrownfieldCommonOptions & Partial; +/** + * Expo config plugin options for Android prebuild scaffolding. + */ +export type BrownfieldExpoAndroidConfig = { + /** + * The package name for the Android library module. + */ + packageName?: string; + + /** + * Minimum SDK version for the Android library. + */ + minSdkVersion?: number; + + /** + * Target SDK version for the Android library. + */ + targetSdkVersion?: number; + + /** + * Compile SDK version for the Android library. + */ + compileSdkVersion?: number; + + /** + * Maven group ID used when publishing the AAR. + */ + groupId?: string; + + /** + * Maven artifact ID used when publishing the AAR. + */ + artifactId?: string; + + /** + * Maven version used when publishing the AAR. + */ + version?: string; +}; + +/** + * Expo config plugin options for iOS prebuild scaffolding. + */ +export type BrownfieldExpoIosConfig = { + /** + * The bundle identifier for the framework. + */ + bundleIdentifier?: string; + + /** + * Custom build settings applied when building the framework. + */ + buildSettings?: Record; + + /** + * Minimum iOS deployment target for the generated framework. + */ + deploymentTarget?: string; + + /** + * Framework version used for Apple build settings. + */ + frameworkVersion?: string; +}; + export type BrownfieldAndroidConfig = Omit< Partial & Partial, keyof BrownfieldCommonOptions ->; +> & { + /** + * Expo config plugin options for Android Expo prebuild phase. + */ + expo?: BrownfieldExpoAndroidConfig; +}; + export type BrownfieldIosConfig = Omit< Partial, keyof BrownfieldCommonOptions ->; +> & { + /** + * Expo config plugin options for iOS Expo prebuild phase. + */ + expo?: BrownfieldExpoIosConfig; +}; export type BrownfieldConfig = BrownfieldConfigMetadata & BrownfieldCommonOptions & diff --git a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts index 9b9079be..3f093fe1 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts @@ -4,7 +4,12 @@ import { type ConfigPlugin, type StaticPlugin, } from '@expo/config-plugins'; -import type { ExpoConfig } from '@expo/config-types'; + +import { + assertNoConfigFilePluginOverlap, + loadBrownfieldConfig, + resolveBrownfieldPluginConfig, +} from '@callstack/brownfield-cli/expo-plugin-config'; import { withBrownfieldIos } from './ios/withBrownfieldIos'; import { withBrownfieldAndroid } from './android/withBrownfieldAndroid'; @@ -15,49 +20,6 @@ import type { import { Logger } from './logging'; -/** - * Resolves the plugin configuration using provided config and config defaults. - * @param config The user-provided Brownfield configuration. - * @param expoConfig The Expo configuration object. - * @returns The resolved Brownfield configuration. - */ -function resolveConfig( - config: BrownfieldPluginConfig = {}, - expoConfig: ExpoConfig -): ResolvedBrownfieldPluginConfig { - Logger.setIsDebug(config.debug ?? false); - - const androidPackage = expoConfig.android?.package; - const androidModuleName = config.android?.moduleName ?? 'brownfieldlib'; - - return { - ios: expoConfig.ios - ? { - frameworkName: config.ios?.frameworkName ?? 'BrownfieldLib', - bundleIdentifier: - config.ios?.bundleIdentifier ?? - `${expoConfig.ios.bundleIdentifier}.brownfield`, - buildSettings: config.ios?.buildSettings ?? {}, - deploymentTarget: config.ios?.deploymentTarget ?? '15.0', - frameworkVersion: config.ios?.frameworkVersion ?? '1', - } - : null, - android: androidPackage - ? { - moduleName: androidModuleName, - packageName: config.android?.packageName ?? androidPackage, - minSdkVersion: config.android?.minSdkVersion ?? 24, - targetSdkVersion: config.android?.targetSdkVersion ?? 35, - compileSdkVersion: config.android?.compileSdkVersion ?? 35, - groupId: config.android?.groupId ?? androidPackage, - artifactId: config.android?.artifactId ?? androidModuleName, - version: config.android?.version ?? '0.0.1-SNAPSHOT', - } - : null, - debug: config.debug ?? false, - }; -} - /** * React Native Brownfield - Expo Config Plugin. * @@ -90,7 +52,15 @@ const withBrownfield: ConfigPlugin = ( config, props = {} ) => { - const resolvedConfig = resolveConfig(props ?? {}, config); + const pluginProps = props ?? {}; + const fileConfig = loadBrownfieldConfig(); + + assertNoConfigFilePluginOverlap(fileConfig, pluginProps); + + const resolvedConfig: ResolvedBrownfieldPluginConfig = + resolveBrownfieldPluginConfig(pluginProps, fileConfig, config); + + Logger.setIsDebug(resolvedConfig.debug); const plugins: (ConfigPlugin | StaticPlugin)[] = [];