Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/vast-waves-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@callstack/react-native-brownfield': patch
'@callstack/brownfield-cli': patch
---

feat: support local BGP in Expo config plugin
22 changes: 12 additions & 10 deletions docs/docs/docs/api-reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Example:
"variant": "release",
"expo": {
"packageName": "com.example.app",
"minSdkVersion": 24
"minSdkVersion": 24,
"useLocalGradlePlugin": true
}
},
"ios": {
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/docs/getting-started/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions packages/cli/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/__tests__/expoPluginConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ describe('resolveBrownfieldPluginConfig', () => {
groupId: 'com.example.app',
artifactId: 'brownfieldlib',
version: '0.0.1-SNAPSHOT',
useLocalGradlePlugin: false,
},
});
});
Expand Down Expand Up @@ -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);
});
});
4 changes: 4 additions & 0 deletions packages/cli/src/expoPluginConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type BrownfieldPluginProps = {
groupId?: string;
artifactId?: string;
version?: string;
useLocalGradlePlugin?: boolean;
};
};

Expand All @@ -35,6 +36,7 @@ export type ResolvedBrownfieldPluginAndroidConfig = {
groupId: string;
artifactId: string;
version: string;
useLocalGradlePlugin: boolean;
};

export type ResolvedBrownfieldPluginIosConfig = {
Expand Down Expand Up @@ -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,
};
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ describe('createAndroidModule', () => {
groupId: 'com.example',
artifactId: 'brownfieldlib',
version: '1.0.0',
useLocalGradlePlugin: false,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -47,24 +91,31 @@ 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(
`Modifying settings.gradle to include module "${moduleName}"`
);

// 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`);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Loading