diff --git a/.changeset/vast-waves-itch.md b/.changeset/vast-waves-itch.md new file mode 100644 index 00000000..88238eab --- /dev/null +++ b/.changeset/vast-waves-itch.md @@ -0,0 +1,6 @@ +--- +'@callstack/react-native-brownfield': patch +'@callstack/brownfield-cli': patch +--- + +feat: support local BGP in Expo config plugin diff --git a/docs/docs/docs/api-reference/configuration.mdx b/docs/docs/docs/api-reference/configuration.mdx index 6757ca4b..0c16c862 100644 --- a/docs/docs/docs/api-reference/configuration.mdx +++ b/docs/docs/docs/api-reference/configuration.mdx @@ -57,7 +57,8 @@ Example: "variant": "release", "expo": { "packageName": "com.example.app", - "minSdkVersion": 24 + "minSdkVersion": 24, + "useLocalGradlePlugin": true } }, "ios": { @@ -176,15 +177,16 @@ All file-based platform options mirror CLI flags, but they use camelCase propert #### 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. | +| 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. | +| `android.expo.useLocalGradlePlugin` | `boolean` | Load the Brownfield Gradle plugin from `node_modules` via `includeBuild` instead of the Maven classpath dependency. Disabled by default. | ### iOS keys diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index e7c7c8fc..4fd2dfca 100644 --- a/docs/docs/docs/getting-started/expo.mdx +++ b/docs/docs/docs/getting-started/expo.mdx @@ -251,3 +251,5 @@ When using a Brownfield config file, register the plugin without options: - Maven artifact ID used when publishing the AAR. - `version` (`string`, default: `"0.0.1-SNAPSHOT"`) - Maven version used when publishing the AAR. +- `useLocalGradlePlugin` (`boolean`, default: `false`) + - Load the Brownfield Gradle plugin from `@callstack/react-native-brownfield/gradle-plugin/brownfield` in `node_modules` via `includeBuild` instead of adding the Maven classpath dependency. In `brownfield.config.*`, use `android.expo.useLocalGradlePlugin` instead. Useful when patching the plugin locally or working with an unreleased version. See [Load the Plugin from Node Modules](/docs/getting-started/android#advanced-load-the-plugin-from-node-modules) for details. diff --git a/packages/cli/schema.json b/packages/cli/schema.json index c2e5b0c2..bbb9246d 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -73,6 +73,10 @@ "description": "Target SDK version for the Android library.", "type": "number" }, + "useLocalGradlePlugin": { + "description": "When true, load the Brownfield Gradle plugin from `@callstack/react-native-brownfield/gradle-plugin/brownfield` via `includeBuild` instead of adding the Maven classpath dependency. Disabled by default.", + "type": "boolean" + }, "version": { "description": "Maven version used when publishing the AAR.", "type": "string" diff --git a/packages/cli/src/__tests__/expoPluginConfig.test.ts b/packages/cli/src/__tests__/expoPluginConfig.test.ts index 8740bf63..6745c71e 100644 --- a/packages/cli/src/__tests__/expoPluginConfig.test.ts +++ b/packages/cli/src/__tests__/expoPluginConfig.test.ts @@ -205,6 +205,7 @@ describe('resolveBrownfieldPluginConfig', () => { groupId: 'com.example.app', artifactId: 'brownfieldlib', version: '0.0.1-SNAPSHOT', + useLocalGradlePlugin: false, }, }); }); @@ -315,4 +316,36 @@ describe('resolveBrownfieldPluginConfig', () => { bundleIdentifier: 'com.example.framework', }); }); + + it('maps android.expo.useLocalGradlePlugin from file config', () => { + const resolved = resolveBrownfieldPluginConfig( + {}, + { + android: { + moduleName: 'mylib', + expo: { + useLocalGradlePlugin: true, + }, + }, + }, + baseExpoConfig + ); + + expect(resolved.android?.useLocalGradlePlugin).toBe(true); + }); + + it('maps android.useLocalGradlePlugin from legacy app.json plugin props', () => { + const resolved = resolveBrownfieldPluginConfig( + { + android: { + moduleName: 'mylib', + useLocalGradlePlugin: true, + }, + }, + null, + baseExpoConfig + ); + + expect(resolved.android?.useLocalGradlePlugin).toBe(true); + }); }); diff --git a/packages/cli/src/expoPluginConfig.ts b/packages/cli/src/expoPluginConfig.ts index 830da182..9b455636 100644 --- a/packages/cli/src/expoPluginConfig.ts +++ b/packages/cli/src/expoPluginConfig.ts @@ -23,6 +23,7 @@ export type BrownfieldPluginProps = { groupId?: string; artifactId?: string; version?: string; + useLocalGradlePlugin?: boolean; }; }; @@ -35,6 +36,7 @@ export type ResolvedBrownfieldPluginAndroidConfig = { groupId: string; artifactId: string; version: string; + useLocalGradlePlugin: boolean; }; export type ResolvedBrownfieldPluginIosConfig = { @@ -185,6 +187,8 @@ export function resolveBrownfieldPluginConfig( groupId: effectiveProps.android?.groupId ?? androidPackage, artifactId: effectiveProps.android?.artifactId ?? androidModuleName, version: effectiveProps.android?.version ?? '0.0.1-SNAPSHOT', + useLocalGradlePlugin: + effectiveProps.android?.useLocalGradlePlugin ?? false, } : null, }; diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 42fe6231..4acc4a1c 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -82,6 +82,14 @@ export type BrownfieldExpoAndroidConfig = { * Maven version used when publishing the AAR. */ version?: string; + + /** + * When true, load the Brownfield Gradle plugin from + * `@callstack/react-native-brownfield/gradle-plugin/brownfield` via + * `includeBuild` instead of adding the Maven classpath dependency. + * Disabled by default. + */ + useLocalGradlePlugin?: boolean; }; /** diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts index 8dc72de2..2aa06f97 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts @@ -183,6 +183,7 @@ describe('createAndroidModule', () => { groupId: 'com.example', artifactId: 'brownfieldlib', version: '1.0.0', + useLocalGradlePlugin: false, }, }; } diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/__tests__/gradleHelpers.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/__tests__/gradleHelpers.test.ts new file mode 100644 index 00000000..4c65b25b --- /dev/null +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/__tests__/gradleHelpers.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import { modifyRootBuildGradle, modifySettingsGradle } from '../gradleHelpers'; + +const rootBuildGradle = ` +buildscript { + ext { + buildToolsVersion = "35.0.0" + } + dependencies { + classpath("com.android.tools.build:gradle:8.6.0") + } +} +`; + +const settingsGradle = `pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +rootProject.name = 'MyApp' +include ':app' +`; + +describe('modifyRootBuildGradle', () => { + it('adds the Maven Brownfield Gradle plugin classpath by default', () => { + const result = modifyRootBuildGradle(rootBuildGradle); + + expect(result).toContain('brownfield-gradle-plugin'); + }); + + it('skips the Maven classpath when useLocalGradlePlugin is enabled', () => { + const result = modifyRootBuildGradle(rootBuildGradle, { + useLocalGradlePlugin: true, + }); + + expect(result).toBe(rootBuildGradle); + expect(result).not.toContain('brownfield-gradle-plugin'); + }); +}); + +describe('modifySettingsGradle', () => { + it('includes the brownfield module', () => { + const result = modifySettingsGradle(settingsGradle, 'brownfieldlib'); + + expect(result).toContain("include ':brownfieldlib'"); + }); + + it('adds includeBuild for the local Brownfield Gradle plugin when enabled', () => { + const result = modifySettingsGradle(settingsGradle, 'brownfieldlib', { + useLocalGradlePlugin: true, + }); + + expect(result).toContain( + 'includeBuild("../node_modules/@callstack/react-native-brownfield/gradle-plugin/brownfield")' + ); + expect(result).toContain("include ':brownfieldlib'"); + }); + + it('prepends pluginManagement when settings.gradle has no pluginManagement block', () => { + const settingsWithoutPluginManagement = `rootProject.name = 'MyApp' +include ':app' +`; + + const result = modifySettingsGradle( + settingsWithoutPluginManagement, + 'brownfieldlib', + { useLocalGradlePlugin: true } + ); + + expect(result.startsWith('pluginManagement {')).toBe(true); + expect(result).toContain( + 'includeBuild("../node_modules/@callstack/react-native-brownfield/gradle-plugin/brownfield")' + ); + }); + + it('does not duplicate the local Brownfield Gradle plugin includeBuild', () => { + const settingsWithLocalPlugin = `${settingsGradle} +includeBuild("../node_modules/@callstack/react-native-brownfield/gradle-plugin/brownfield") +`; + + const result = modifySettingsGradle( + settingsWithLocalPlugin, + 'brownfieldlib', + { + useLocalGradlePlugin: true, + } + ); + + expect( + result.match( + /includeBuild\("\.\.\/node_modules\/@callstack\/react-native-brownfield\/gradle-plugin\/brownfield"\)/g + ) + ).toHaveLength(1); + }); +}); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts index e6a2f05b..990a0f65 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/utils/gradleHelpers.ts @@ -1,12 +1,29 @@ import { brownfieldGradlePluginDependency } from './constants'; import { Logger } from '../../logging'; +const LOCAL_GRADLE_PLUGIN_INCLUDE_BUILD = + 'includeBuild("../node_modules/@callstack/react-native-brownfield/gradle-plugin/brownfield")'; + +type GradleModificationOptions = { + useLocalGradlePlugin?: boolean; +}; + /** * Modifies the root build.gradle to add the Brownfield Gradle plugin dependency * @param contents The original build.gradle content * @returns The modified build.gradle content */ -export function modifyRootBuildGradle(contents: string): string { +export function modifyRootBuildGradle( + contents: string, + { useLocalGradlePlugin = false }: GradleModificationOptions = {} +): string { + if (useLocalGradlePlugin) { + Logger.logDebug( + 'Skipping Maven Brownfield Gradle plugin classpath because useLocalGradlePlugin is enabled' + ); + return contents; + } + // check if already added if (contents.includes('brownfield-gradle-plugin')) { Logger.logDebug( @@ -39,6 +56,33 @@ export function modifyRootBuildGradle(contents: string): string { return modifiedContents; } +function addLocalGradlePluginIncludeBuild(contents: string): string { + if (contents.includes('gradle-plugin/brownfield')) { + Logger.logDebug( + 'Local Brownfield Gradle plugin includeBuild already present, skipping' + ); + return contents; + } + + Logger.logDebug( + 'Modifying settings.gradle to include local Brownfield Gradle plugin' + ); + + const pluginManagementMatch = contents.match(/pluginManagement\s*\{/); + + if (pluginManagementMatch?.index !== undefined) { + const insertIndex = + pluginManagementMatch.index + pluginManagementMatch[0].length; + const insertion = `\n\t${LOCAL_GRADLE_PLUGIN_INCLUDE_BUILD}`; + + return ( + contents.slice(0, insertIndex) + insertion + contents.slice(insertIndex) + ); + } + + return `pluginManagement {\n\t${LOCAL_GRADLE_PLUGIN_INCLUDE_BUILD}\n}\n\n${contents}`; +} + /** * Modifies settings.gradle to include the Brownfield module * @param contents The original settings.gradle content @@ -47,16 +91,23 @@ export function modifyRootBuildGradle(contents: string): string { */ export function modifySettingsGradle( contents: string, - moduleName: string + moduleName: string, + { useLocalGradlePlugin = false }: GradleModificationOptions = {} ): string { + let modifiedContents = contents; + + if (useLocalGradlePlugin) { + modifiedContents = addLocalGradlePluginIncludeBuild(modifiedContents); + } + const includeStatement = `include ':${moduleName}'`; // check if already included - if (contents.includes(includeStatement)) { + if (modifiedContents.includes(includeStatement)) { Logger.logDebug( `Module "${moduleName}" already in settings.gradle, skipping` ); - return contents; + return modifiedContents; } Logger.logDebug( @@ -64,7 +115,7 @@ export function modifySettingsGradle( ); // add the include statement at the end - const modifiedContents = contents + `\n${includeStatement}\n`; + modifiedContents = modifiedContents + `\n${includeStatement}\n`; Logger.logDebug(`Added module "${moduleName}" to settings.gradle`); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts index 9ff3c2d9..68aa8a6e 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts @@ -32,7 +32,8 @@ export const withBrownfieldAndroid: ConfigPlugin< // Step 1: modify root build.gradle to add Brownfield Gradle plugin dependency config = withProjectBuildGradle(config, (gradleConfig) => { gradleConfig.modResults.contents = modifyRootBuildGradle( - gradleConfig.modResults.contents + gradleConfig.modResults.contents, + { useLocalGradlePlugin: androidConfig.useLocalGradlePlugin } ); return gradleConfig; @@ -42,7 +43,8 @@ export const withBrownfieldAndroid: ConfigPlugin< config = withSettingsGradle(config, (settingsConfig) => { settingsConfig.modResults.contents = modifySettingsGradle( settingsConfig.modResults.contents, - androidConfig.moduleName + androidConfig.moduleName, + { useLocalGradlePlugin: androidConfig.useLocalGradlePlugin } ); return settingsConfig; diff --git a/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts b/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts index cd9f9f4d..68f1b622 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts @@ -49,6 +49,13 @@ export interface BrownfieldPluginAndroidConfig { * @default "0.0.1-SNAPSHOT" */ version?: string; + + /** + * Load the Brownfield Gradle plugin from node_modules via includeBuild + * instead of the Maven classpath dependency. + * @default false + */ + useLocalGradlePlugin?: boolean; } /**