expo-brownfield 57.0.6 → 57.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/android/build.gradle +2 -2
- package/cli/src/commands/build-android.ts +2 -1
- package/cli/src/index.ts +1 -0
- package/cli/src/utils/android.ts +37 -6
- package/cli/src/utils/config.ts +61 -6
- package/cli/src/utils/error.ts +2 -1
- package/cli/src/utils/prebuild.ts +21 -7
- package/cli/src/utils/types.ts +1 -0
- package/gradle-plugins/brownfield/src/main/kotlin/expo/modules/plugin/ExpoBrownfieldSetupPlugin.kt +56 -0
- package/gradle-plugins/publish/build.gradle.kts +4 -1
- package/gradle-plugins/publish/src/main/kotlin/expo/modules/plugin/prebuilts.kt +8 -0
- package/package.json +6 -6
- package/plugin/src/android/plugins/withGradlePropertiesPlugin.ts +53 -0
- package/plugin/src/android/plugins/withProjectBuildGradlePlugin.ts +47 -11
- package/plugin/src/android/plugins/withProjectFilesPlugin.ts +33 -1
- package/plugin/src/android/plugins/withSettingsGradlePlugin.ts +22 -4
- package/plugin/src/common/filesystem.ts +5 -0
- package/plugin/templates/android/BrownfieldActivity.kt +26 -1
- package/plugin/templates/android/ReactNativeHostManager.kt +7 -1
- package/plugin/templates/android/consumer-rules.pro +67 -0
- package/plugin/templates/android/fused/build.gradle.kts +580 -0
- package/plugin/templates/patches/BrownfieldActivity.patch +11 -7
- package/plugin/templates/patches/ReactNativeHostManager.patch +6 -6
- package/prebuilds/output/debug/xcframeworks/ExpoBrownfield.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoBrownfield.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.7 — 2026-07-22
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [android] Add support for publishing a single fused AAR via AGP Fused Library ([#47921](https://github.com/expo/expo/pull/45878) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
18
|
+
|
|
13
19
|
## 57.0.6 — 2026-07-17
|
|
14
20
|
|
|
15
21
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -4,7 +4,7 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'expo.modules.brownfield'
|
|
7
|
-
version = '57.0.
|
|
7
|
+
version = '57.0.7'
|
|
8
8
|
|
|
9
9
|
expoModule {
|
|
10
10
|
canBePublished false
|
|
@@ -14,7 +14,7 @@ android {
|
|
|
14
14
|
namespace "expo.modules.brownfield"
|
|
15
15
|
defaultConfig {
|
|
16
16
|
versionCode 1
|
|
17
|
-
versionName '57.0.
|
|
17
|
+
versionName '57.0.7'
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -17,8 +17,9 @@ const buildAndroid = async (command: Command) => {
|
|
|
17
17
|
}
|
|
18
18
|
printAndroidConfig(config);
|
|
19
19
|
|
|
20
|
+
const extraGradleArgs = config.fused ? ['-Pbrownfield.fused=true'] : [];
|
|
20
21
|
for (const task of config.tasks) {
|
|
21
|
-
await runTask(task, config.verbose, config.dryRun);
|
|
22
|
+
await runTask(task, config.verbose, config.dryRun, extraGradleArgs);
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
|
package/cli/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ program
|
|
|
16
16
|
.option('-d, --debug', 'build debug variant')
|
|
17
17
|
.option('-r, --release', 'build release variant')
|
|
18
18
|
.option('-a, --all', 'build both debug and release variants')
|
|
19
|
+
.option('--fused', 'publish a single fat AAR per variant via AGP Fused Library')
|
|
19
20
|
.option('--verbose', 'forward all output to the terminal')
|
|
20
21
|
.option('-l, --library <library>', 'name of the brownfield library')
|
|
21
22
|
.option('-t, --task <task...>', 'publishing task to be run (multiple can be passed)')
|
package/cli/src/utils/android.ts
CHANGED
|
@@ -7,9 +7,21 @@ import CLIError from './error';
|
|
|
7
7
|
import { withSpinner } from './spinner';
|
|
8
8
|
import type { AndroidConfig, BuildVariant } from './types';
|
|
9
9
|
|
|
10
|
-
export const buildPublishingTask = (
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export const buildPublishingTask = (
|
|
11
|
+
variant: BuildVariant,
|
|
12
|
+
repository: string,
|
|
13
|
+
fusedOpts: { fused: boolean; library: string } = { fused: false, library: '' }
|
|
14
|
+
): string => {
|
|
15
|
+
const repositoryName =
|
|
16
|
+
repository.toLowerCase() === 'mavenlocal' ? 'MavenLocal' : `${repository}Repository`;
|
|
17
|
+
const task = `publishBrownfield${variant}PublicationTo${repositoryName}`;
|
|
18
|
+
// In `--fused` mode, route the task to the matching sibling subproject:
|
|
19
|
+
// `:<lib>-fused-release` for Release, `:<lib>-fused-debug` for Debug.
|
|
20
|
+
if (fusedOpts.fused) {
|
|
21
|
+
const siblingSuffix = variant === 'Debug' ? 'debug' : 'release';
|
|
22
|
+
return `:${fusedOpts.library}-fused-${siblingSuffix}:${task}`;
|
|
23
|
+
}
|
|
24
|
+
return task;
|
|
13
25
|
};
|
|
14
26
|
|
|
15
27
|
export const findBrownfieldLibrary = (): string | undefined => {
|
|
@@ -59,6 +71,7 @@ export const printAndroidConfig = (config: AndroidConfig) => {
|
|
|
59
71
|
console.log(chalk.bold('Resolved build configuration'));
|
|
60
72
|
console.log(` - Build variant: ${chalk.blue(config.variant)}`);
|
|
61
73
|
console.log(` - Library: ${chalk.blue(config.library)}`);
|
|
74
|
+
console.log(` - Fused: ${chalk.blue(config.fused)}`);
|
|
62
75
|
console.log(` - Verbose: ${chalk.blue(config.verbose)}`);
|
|
63
76
|
console.log(` - Dry run: ${chalk.blue(config.dryRun)}`);
|
|
64
77
|
console.log(` - Tasks:`);
|
|
@@ -93,15 +106,33 @@ export const processTasks = (stdout: string): string[] => {
|
|
|
93
106
|
);
|
|
94
107
|
};
|
|
95
108
|
|
|
96
|
-
export const runTask = async (
|
|
109
|
+
export const runTask = async (
|
|
110
|
+
task: string,
|
|
111
|
+
verbose: boolean,
|
|
112
|
+
dryRun: boolean,
|
|
113
|
+
extraGradleArgs: string[] = []
|
|
114
|
+
) => {
|
|
115
|
+
// Fused-shaped tasks (e.g. passed manually via -t without --fused) must still
|
|
116
|
+
// activate fused mode in Gradle: without `-Pbrownfield.fused=true` the fused
|
|
117
|
+
// sibling subprojects are inert (no publications) and the conditional AGP
|
|
118
|
+
// force-bump in the root build.gradle never applies, so the build would fail
|
|
119
|
+
// mid-execution under the version catalog's AGP.
|
|
120
|
+
const fusedProperty = '-Pbrownfield.fused=true';
|
|
121
|
+
const isFusedTask = /(?:^|:)[^:\s]+-fused-(?:release|debug):/.test(task);
|
|
122
|
+
const perTaskArgs =
|
|
123
|
+
isFusedTask && !extraGradleArgs.includes(fusedProperty)
|
|
124
|
+
? [...extraGradleArgs, fusedProperty]
|
|
125
|
+
: extraGradleArgs;
|
|
126
|
+
|
|
127
|
+
const args = [task, ...perTaskArgs];
|
|
97
128
|
if (dryRun) {
|
|
98
|
-
console.log(`./gradlew ${
|
|
129
|
+
console.log(`./gradlew ${args.join(' ')}`);
|
|
99
130
|
return;
|
|
100
131
|
}
|
|
101
132
|
|
|
102
133
|
return withSpinner({
|
|
103
134
|
operation: () =>
|
|
104
|
-
spawnAsync('./gradlew',
|
|
135
|
+
spawnAsync('./gradlew', args, {
|
|
105
136
|
cwd: path.join(process.cwd(), 'android'),
|
|
106
137
|
stdio: verbose ? 'inherit' : 'pipe',
|
|
107
138
|
}),
|
package/cli/src/utils/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OptionValues } from 'commander';
|
|
2
|
+
import { getConfig } from 'expo/config';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
@@ -16,12 +17,15 @@ import type {
|
|
|
16
17
|
import { HOST_PROVIDED_FRAMEWORKS_KEY } from '../../../shared/build';
|
|
17
18
|
|
|
18
19
|
export const resolveBuildConfigAndroid = (options: OptionValues): AndroidConfig => {
|
|
19
|
-
const
|
|
20
|
+
const fused = !!options.fused;
|
|
21
|
+
const variant: BuildVariant = resolveVariant(options);
|
|
22
|
+
const library = resolveLibrary(options);
|
|
20
23
|
return {
|
|
21
24
|
...resolveCommonConfig(options),
|
|
22
|
-
library
|
|
23
|
-
tasks: resolveTaskArray(options, variant),
|
|
25
|
+
library,
|
|
26
|
+
tasks: resolveTaskArray(options, variant, { fused, library }),
|
|
24
27
|
variant,
|
|
28
|
+
fused,
|
|
25
29
|
};
|
|
26
30
|
};
|
|
27
31
|
|
|
@@ -157,15 +161,66 @@ const resolveLibrary = (options: OptionValues): string => {
|
|
|
157
161
|
return options.library || findBrownfieldLibrary();
|
|
158
162
|
};
|
|
159
163
|
|
|
160
|
-
const resolveTaskArray = (
|
|
164
|
+
const resolveTaskArray = (
|
|
165
|
+
options: OptionValues,
|
|
166
|
+
variant: BuildVariant,
|
|
167
|
+
fusedOpts: { fused: boolean; library: string }
|
|
168
|
+
): string[] => {
|
|
161
169
|
const tasks: string[] = options.task ?? [];
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
let repositories: string[] = options.repository ?? [];
|
|
171
|
+
if (tasks.length === 0 && repositories.length === 0) {
|
|
172
|
+
repositories = resolveLocalRepositoriesFromAppConfig(!!options.verbose);
|
|
173
|
+
if (repositories.length > 0) {
|
|
174
|
+
console.info(
|
|
175
|
+
`No --repo or --task specified; defaulting to local repositories from the app config: ${repositories.join(', ')}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// In `--fused` mode, `--all` expands to separate Debug + Release task
|
|
180
|
+
// invocations against the matching sibling subprojects.
|
|
181
|
+
const variantsForRepoTasks: BuildVariant[] =
|
|
182
|
+
fusedOpts.fused && variant === 'All' ? ['Debug', 'Release'] : [variant];
|
|
183
|
+
const repoTasks = repositories.flatMap((repo) =>
|
|
184
|
+
variantsForRepoTasks.map((v) => buildPublishingTask(v, repo, fusedOpts))
|
|
164
185
|
);
|
|
165
186
|
|
|
166
187
|
return Array.from(new Set([...tasks, ...repoTasks]));
|
|
167
188
|
};
|
|
168
189
|
|
|
190
|
+
const resolveLocalRepositoriesFromAppConfig = (verbose: boolean): string[] => {
|
|
191
|
+
let publishing: unknown;
|
|
192
|
+
try {
|
|
193
|
+
const { exp } = getConfig(process.cwd(), { skipSDKVersionRequirement: true });
|
|
194
|
+
const plugin = exp.plugins?.find(
|
|
195
|
+
(entry): entry is [string, any] => Array.isArray(entry) && entry[0] === 'expo-brownfield'
|
|
196
|
+
);
|
|
197
|
+
publishing = plugin?.[1]?.android?.publishing;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// App config could not be evaluated here — fall through to the plugin's
|
|
200
|
+
// default publishing target below.
|
|
201
|
+
if (verbose) {
|
|
202
|
+
console.warn(
|
|
203
|
+
`Could not read \`android.publishing\` from the app config, falling back to MavenLocal: ${
|
|
204
|
+
error instanceof Error ? error.message : String(error)
|
|
205
|
+
}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const entries =
|
|
211
|
+
Array.isArray(publishing) && publishing.length > 0 ? publishing : [{ type: 'localMaven' }];
|
|
212
|
+
const repositories = entries.flatMap((entry) => {
|
|
213
|
+
if (entry?.type === 'localMaven') {
|
|
214
|
+
return ['MavenLocal'];
|
|
215
|
+
}
|
|
216
|
+
if (entry?.type === 'localDirectory' && typeof entry?.name === 'string') {
|
|
217
|
+
return [entry.name];
|
|
218
|
+
}
|
|
219
|
+
return [];
|
|
220
|
+
});
|
|
221
|
+
return Array.from(new Set(repositories));
|
|
222
|
+
};
|
|
223
|
+
|
|
169
224
|
const resolveVariant = (options: OptionValues): BuildVariant => {
|
|
170
225
|
let variant: BuildVariant = 'All';
|
|
171
226
|
if (options.release && !options.debug) {
|
package/cli/src/utils/error.ts
CHANGED
|
@@ -20,7 +20,8 @@ type ErrorType =
|
|
|
20
20
|
class CLIError {
|
|
21
21
|
private static readonly errorSymbol: string = '✖';
|
|
22
22
|
private static readonly errorMessages: Record<ErrorType, string> = {
|
|
23
|
-
'android-task-repo':
|
|
23
|
+
'android-task-repo':
|
|
24
|
+
'At least one task or repository must be specified. The app config declares no local Maven repositories to default to, and remote repositories are never published to implicitly — pass `--repo <name>` or `-t <gradle task>` explicitly.',
|
|
24
25
|
'android-directory-not-found': 'Cannot find `android` directory in the project',
|
|
25
26
|
'android-library-unknown-error': 'Unknown error occurred while finding brownfield library',
|
|
26
27
|
'android-library-not-found': 'Could not find brownfield library in the project',
|
|
@@ -17,14 +17,24 @@ export const validatePrebuild = async (
|
|
|
17
17
|
|
|
18
18
|
if (!checkPrebuild(platform)) {
|
|
19
19
|
console.info(`${chalk.yellow(`⚠ Prebuild for platform: ${platform} is missing`)}`);
|
|
20
|
-
const response = await prompts({
|
|
21
|
-
type: 'confirm',
|
|
22
|
-
name: 'shouldRunPrebuild',
|
|
23
|
-
message: 'Do you want to run the prebuild now?',
|
|
24
|
-
initial: false,
|
|
25
|
-
});
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
let shouldRunPrebuild: boolean;
|
|
22
|
+
if (isInteractive()) {
|
|
23
|
+
const response = await prompts({
|
|
24
|
+
type: 'confirm',
|
|
25
|
+
name: 'shouldRunPrebuild',
|
|
26
|
+
message: 'Do you want to run the prebuild now?',
|
|
27
|
+
initial: false,
|
|
28
|
+
});
|
|
29
|
+
shouldRunPrebuild = !!response.shouldRunPrebuild;
|
|
30
|
+
} else {
|
|
31
|
+
console.info(
|
|
32
|
+
`Non-interactive shell detected; running \`npx expo prebuild --platform ${platform}\` automatically`
|
|
33
|
+
);
|
|
34
|
+
shouldRunPrebuild = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (shouldRunPrebuild) {
|
|
28
38
|
await withSpinner({
|
|
29
39
|
operation: () => spawnAsync('npx', ['expo', 'prebuild', '--platform', platform]),
|
|
30
40
|
loaderMessage: `Running 'npx expo prebuild' for platform: ${platform}...`,
|
|
@@ -109,3 +119,7 @@ const checkPrebuild = (platform: Platform): boolean => {
|
|
|
109
119
|
const nativeDirectory = path.join(process.cwd(), platform);
|
|
110
120
|
return fs.existsSync(nativeDirectory);
|
|
111
121
|
};
|
|
122
|
+
|
|
123
|
+
const isInteractive = (): boolean => {
|
|
124
|
+
return !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
125
|
+
};
|
package/cli/src/utils/types.ts
CHANGED
package/gradle-plugins/brownfield/src/main/kotlin/expo/modules/plugin/ExpoBrownfieldSetupPlugin.kt
CHANGED
|
@@ -12,6 +12,7 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
|
|
|
12
12
|
override fun apply(project: Project) {
|
|
13
13
|
project.evaluationDependsOn(":expo")
|
|
14
14
|
setupDependencySubstitution(project)
|
|
15
|
+
setupFusedModeStripping(project)
|
|
15
16
|
|
|
16
17
|
project.afterEvaluate { project ->
|
|
17
18
|
setupSourceSets(project)
|
|
@@ -23,6 +24,61 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* In `--fused` builds, strips entries from the autolinking-generated
|
|
29
|
+
* `ExpoModulesPackageList.kt` by package prefix, via
|
|
30
|
+
* `-Pbrownfield.fused.strip-packages=expo.modules.foo.,expo.modules.bar.`.
|
|
31
|
+
*
|
|
32
|
+
* This is the companion escape hatch to `-Pbrownfield.fused.skip=<project,...>`
|
|
33
|
+
* in the fused sibling's build script: a module excluded from the fat AAR still
|
|
34
|
+
* has its `Package` class referenced by `ExpoModulesPackageList.<clinit>`, which
|
|
35
|
+
* would crash the host with `NoClassDefFoundError` at startup — stripping the
|
|
36
|
+
* matching entries here keeps the package list consistent with the AAR contents.
|
|
37
|
+
* Inert unless both properties are set.
|
|
38
|
+
*/
|
|
39
|
+
private fun setupFusedModeStripping(brownfieldProject: Project) {
|
|
40
|
+
if (brownfieldProject.findProperty("brownfield.fused") != "true") return
|
|
41
|
+
|
|
42
|
+
val expoProject = brownfieldProject.rootProject.findProject(":expo") ?: return
|
|
43
|
+
|
|
44
|
+
val stripPrefixes =
|
|
45
|
+
(brownfieldProject.findProperty("brownfield.fused.strip-packages") as? String)
|
|
46
|
+
?.split(',')
|
|
47
|
+
?.map { it.trim() }
|
|
48
|
+
?.filter { it.isNotEmpty() }
|
|
49
|
+
?.toSet()
|
|
50
|
+
?: emptySet()
|
|
51
|
+
|
|
52
|
+
if (stripPrefixes.isEmpty()) return
|
|
53
|
+
|
|
54
|
+
// `:expo` is already evaluated (forced by `evaluationDependsOn(":expo")` at the
|
|
55
|
+
// top of `apply`), so we read the task directly — no afterEvaluate needed.
|
|
56
|
+
val task = expoProject.tasks.findByName("generatePackagesList") ?: return
|
|
57
|
+
task.doLast {
|
|
58
|
+
val outputFile =
|
|
59
|
+
expoProject.layout.buildDirectory
|
|
60
|
+
.file("generated/expo/src/main/java/expo/modules/ExpoModulesPackageList.kt")
|
|
61
|
+
.get()
|
|
62
|
+
.asFile
|
|
63
|
+
if (!outputFile.exists()) {
|
|
64
|
+
expoProject.logger.warn(
|
|
65
|
+
"brownfield.fused: ExpoModulesPackageList.kt not found at ${outputFile.path}; nothing to strip"
|
|
66
|
+
)
|
|
67
|
+
return@doLast
|
|
68
|
+
}
|
|
69
|
+
val original = outputFile.readText()
|
|
70
|
+
val stripped =
|
|
71
|
+
original
|
|
72
|
+
.lineSequence()
|
|
73
|
+
.filter { line -> stripPrefixes.none { prefix -> line.contains(prefix) } }
|
|
74
|
+
.joinToString("\n")
|
|
75
|
+
outputFile.writeText(stripped)
|
|
76
|
+
expoProject.logger.lifecycle(
|
|
77
|
+
"brownfield.fused: stripped ExpoModulesPackageList entries matching ${stripPrefixes.joinToString(", ")}"
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
26
82
|
/**
|
|
27
83
|
* Setup the dependency substitution for `react-android` and `hermes-android`.
|
|
28
84
|
*
|
|
@@ -14,7 +14,10 @@ dependencies {
|
|
|
14
14
|
implementation("expo.modules:expo-autolinking-plugin-shared")
|
|
15
15
|
implementation("org.json:json:20250517")
|
|
16
16
|
implementation(gradleApi())
|
|
17
|
-
|
|
17
|
+
// Matches FUSED_AGP_VERSION in the config plugin — the newest AGP the plugin can
|
|
18
|
+
// run against (forced in fused mode). Compiling against a newer major than the
|
|
19
|
+
// runtime AGP risks NoSuchMethodError on changed APIs.
|
|
20
|
+
compileOnly("com.android.tools.build:gradle:8.13.0")
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
java {
|
|
@@ -13,6 +13,14 @@ import org.gradle.api.tasks.TaskProvider
|
|
|
13
13
|
*/
|
|
14
14
|
internal fun setupPrebuiltsCopying(rootProject: Project) {
|
|
15
15
|
rootProject.afterEvaluate {
|
|
16
|
+
// In `--fused` mode the user is publishing a single fat AAR via Fused Library that
|
|
17
|
+
// already merges every Expo module's classes/resources/jni inside it — the
|
|
18
|
+
// per-module mavenLocal/remote re-publish loop below would emit redundant
|
|
19
|
+
// coordinates next to the fat AAR. Skip.
|
|
20
|
+
if (rootProject.findProperty("brownfield.fused") == "true") {
|
|
21
|
+
return@afterEvaluate
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
val configExtension = getConfigExtension(rootProject)
|
|
17
25
|
|
|
18
26
|
if (configExtension.publications.isEmpty) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-brownfield",
|
|
3
|
-
"version": "57.0.
|
|
3
|
+
"version": "57.0.7",
|
|
4
4
|
"description": "Toolkit and APIs for adding brownfield setup to Expo projects",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"chalk": "^4.1.2",
|
|
42
42
|
"commander": "^14.0.3",
|
|
43
43
|
"diff": "^5.2.0",
|
|
44
|
-
"expo-build-properties": "~57.0.
|
|
44
|
+
"expo-build-properties": "~57.0.7",
|
|
45
45
|
"expo-manifests": "~57.0.1",
|
|
46
46
|
"ora": "^5.4.1",
|
|
47
47
|
"prompts": "^2.4.2"
|
|
@@ -53,15 +53,15 @@
|
|
|
53
53
|
"@types/prompts": "^2.0.6",
|
|
54
54
|
"@types/react": "~19.2.0",
|
|
55
55
|
"glob": "^13.0.6",
|
|
56
|
-
"expo": "57.0.
|
|
57
|
-
"expo
|
|
58
|
-
"
|
|
56
|
+
"expo": "57.0.8",
|
|
57
|
+
"create-expo": "5.0.0",
|
|
58
|
+
"expo-module-scripts": "56.0.3"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"expo": "*",
|
|
62
62
|
"react": "*"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "a4789f1e53353f4929b0baddcfe5a7c622b99c71",
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "expo-module build",
|
|
67
67
|
"build:shared": "expo-module build shared",
|
|
@@ -14,10 +14,63 @@ const withGradlePropertiesPlugin: ConfigPlugin = (config) => {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Opt into AGP's Fused Library Preview. The `.publicationOnly=false` flag lets
|
|
18
|
+
// `include(project(...))` resolve sibling subprojects directly (no mavenLocal
|
|
19
|
+
// round-trip). No-op in non-fused mode.
|
|
20
|
+
const hasFusedOptIn = config.modResults.some(
|
|
21
|
+
(item) =>
|
|
22
|
+
item.type === 'property' && item.key === 'android.experimental.fusedLibrarySupport'
|
|
23
|
+
);
|
|
24
|
+
const hasFusedPubFlag = config.modResults.some(
|
|
25
|
+
(item) =>
|
|
26
|
+
item.type === 'property' &&
|
|
27
|
+
item.key === 'android.experimental.fusedLibrarySupport.publicationOnly'
|
|
28
|
+
);
|
|
29
|
+
if (!hasFusedOptIn || !hasFusedPubFlag) {
|
|
30
|
+
config.modResults = [
|
|
31
|
+
...config.modResults,
|
|
32
|
+
...getFusedLibrarySupportConfiguration(hasFusedOptIn, hasFusedPubFlag),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
return config;
|
|
18
37
|
});
|
|
19
38
|
};
|
|
20
39
|
|
|
40
|
+
const getFusedLibrarySupportConfiguration = (
|
|
41
|
+
hasFusedOptIn: boolean,
|
|
42
|
+
hasFusedPubFlag: boolean
|
|
43
|
+
): AndroidConfig.Properties.PropertiesItem[] => {
|
|
44
|
+
const items: AndroidConfig.Properties.PropertiesItem[] = [];
|
|
45
|
+
if (!hasFusedOptIn) {
|
|
46
|
+
items.push(
|
|
47
|
+
{
|
|
48
|
+
type: 'comment',
|
|
49
|
+
value: "Acknowledge AGP Fused Library Preview status (required to apply the plugin)",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'property',
|
|
53
|
+
key: 'android.experimental.fusedLibrarySupport',
|
|
54
|
+
value: 'true',
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!hasFusedPubFlag) {
|
|
59
|
+
items.push(
|
|
60
|
+
{
|
|
61
|
+
type: 'comment',
|
|
62
|
+
value: 'Allow `com.android.fused-library` to include sibling project deps directly',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'property',
|
|
66
|
+
key: 'android.experimental.fusedLibrarySupport.publicationOnly',
|
|
67
|
+
value: 'false',
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return items;
|
|
72
|
+
};
|
|
73
|
+
|
|
21
74
|
const getDevMenuReleaseConfiguration = (): AndroidConfig.Properties.PropertiesItem[] => {
|
|
22
75
|
return [
|
|
23
76
|
{
|
|
@@ -7,27 +7,63 @@ const EXPO_APPLY_STATEMENT = 'apply plugin: "expo-root-project"';
|
|
|
7
7
|
const PLUGIN_CLASSPATH = 'expo.modules:publish';
|
|
8
8
|
const PLUGIN_NAME = 'expo-brownfield-publish';
|
|
9
9
|
|
|
10
|
+
// AGP 8.12.0's Fused Library plugin's `rewriteClasses` task uses an ASM API version
|
|
11
|
+
// below 9 at the call site and can't read `PermittedSubclasses` bytecode attributes
|
|
12
|
+
// emitted for every Kotlin `sealed class`. AGP 8.13.0+ raises the hardcoded API
|
|
13
|
+
// version. Bump only when the user opts into fused mode via `--fused` (which passes
|
|
14
|
+
// `-Pbrownfield.fused=true`), so default builds keep using the version catalog's AGP.
|
|
15
|
+
const FUSED_AGP_VERSION = '8.13.0';
|
|
16
|
+
const FUSED_AGP_MARKER = 'brownfield.fused';
|
|
17
|
+
|
|
10
18
|
const withProjectBuildGradlePlugin: ConfigPlugin<PluginConfig> = (config, pluginConfig) => {
|
|
11
19
|
return withProjectBuildGradle(config, (config) => {
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
let lines = config.modResults.contents.split('\n');
|
|
21
|
+
|
|
22
|
+
if (!config.modResults.contents.includes(FUSED_AGP_MARKER)) {
|
|
23
|
+
lines = addFusedAgpResolutionStrategy(lines);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!config.modResults.contents.includes(PLUGIN_CLASSPATH)) {
|
|
27
|
+
lines = addPluginClasspathStatement(lines);
|
|
28
|
+
lines = addApplyStatement(lines);
|
|
29
|
+
lines = addPublicationConfiguration(
|
|
30
|
+
lines,
|
|
31
|
+
pluginConfig.publishing,
|
|
32
|
+
pluginConfig.projectRoot,
|
|
33
|
+
pluginConfig.libraryName
|
|
34
|
+
);
|
|
14
35
|
}
|
|
15
36
|
|
|
16
|
-
let lines = config.modResults.contents.split('\n');
|
|
17
|
-
lines = addPluginClasspathStatement(lines);
|
|
18
|
-
lines = addApplyStatement(lines);
|
|
19
|
-
lines = addPublicationConfiguration(
|
|
20
|
-
lines,
|
|
21
|
-
pluginConfig.publishing,
|
|
22
|
-
pluginConfig.projectRoot,
|
|
23
|
-
pluginConfig.libraryName
|
|
24
|
-
);
|
|
25
37
|
config.modResults.contents = lines.join('\n');
|
|
26
38
|
|
|
27
39
|
return config;
|
|
28
40
|
});
|
|
29
41
|
};
|
|
30
42
|
|
|
43
|
+
const addFusedAgpResolutionStrategy = (lines: string[]): string[] => {
|
|
44
|
+
// Appended as a sibling top-level `buildscript { ... }` block. Gradle merges multiple
|
|
45
|
+
// top-level buildscript blocks, so this composes with the project's existing one
|
|
46
|
+
// without clobbering it. The `findProperty` check makes the override conditional on
|
|
47
|
+
// `-Pbrownfield.fused=true` (set by `expo-brownfield build:android --fused`); without
|
|
48
|
+
// the flag the resolution-strategy block never runs and AGP stays at the catalog
|
|
49
|
+
// version.
|
|
50
|
+
const block = [
|
|
51
|
+
'',
|
|
52
|
+
'buildscript {',
|
|
53
|
+
' // expo-brownfield: bump AGP only when `--fused` is active (CLI passes',
|
|
54
|
+
` // -P${FUSED_AGP_MARKER}=true). AGP 8.12.0's Fused Library can't read Kotlin`,
|
|
55
|
+
' // sealed-class bytecode (PermittedSubclasses requires ASM API >= 9, fixed',
|
|
56
|
+
` // in AGP ${FUSED_AGP_VERSION}).`,
|
|
57
|
+
` if (findProperty('${FUSED_AGP_MARKER}') == 'true') {`,
|
|
58
|
+
' configurations.classpath {',
|
|
59
|
+
` resolutionStrategy.force 'com.android.tools.build:gradle:${FUSED_AGP_VERSION}'`,
|
|
60
|
+
' }',
|
|
61
|
+
' }',
|
|
62
|
+
'}',
|
|
63
|
+
];
|
|
64
|
+
return [...lines, ...block];
|
|
65
|
+
};
|
|
66
|
+
|
|
31
67
|
const addPluginClasspathStatement = (lines: string[]): string[] => {
|
|
32
68
|
const statement = ` classpath('${PLUGIN_CLASSPATH}')`;
|
|
33
69
|
const lastClasspathIndex = lines.findLastIndex((line) => line.includes('classpath('));
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { applyPatchToFile, checkPlugin, mkdir } from '../../common';
|
|
5
5
|
import type { PluginConfig } from '../types';
|
|
6
|
-
import { createFileFromTemplate } from '../utils';
|
|
6
|
+
import { createFileFromTemplate, createFileFromTemplateAs } from '../utils';
|
|
7
7
|
|
|
8
8
|
const withProjectFilesPlugin: ConfigPlugin<PluginConfig> = (config, pluginConfig) => {
|
|
9
9
|
return withAndroidManifest(config, (config) => {
|
|
@@ -15,9 +15,20 @@ const withProjectFilesPlugin: ConfigPlugin<PluginConfig> = (config, pluginConfig
|
|
|
15
15
|
const brownfieldMainPath = path.join(brownfieldPath, 'src/main/');
|
|
16
16
|
const brownfieldSourcesPath = path.join(brownfieldMainPath, pluginConfig.packagePath);
|
|
17
17
|
|
|
18
|
+
const fusedReleasePath = path.join(
|
|
19
|
+
pluginConfig.projectRoot,
|
|
20
|
+
`android/${pluginConfig.libraryName}-fused-release`
|
|
21
|
+
);
|
|
22
|
+
const fusedDebugPath = path.join(
|
|
23
|
+
pluginConfig.projectRoot,
|
|
24
|
+
`android/${pluginConfig.libraryName}-fused-debug`
|
|
25
|
+
);
|
|
26
|
+
|
|
18
27
|
// Create directory for the brownfield library sources
|
|
19
28
|
// (and all intermediate directories)
|
|
20
29
|
mkdir(brownfieldSourcesPath, true);
|
|
30
|
+
mkdir(fusedReleasePath, true);
|
|
31
|
+
mkdir(fusedDebugPath, true);
|
|
21
32
|
|
|
22
33
|
// Add files from templates to the brownfield library:
|
|
23
34
|
// - AndroidManifest.xml
|
|
@@ -49,6 +60,27 @@ const withProjectFilesPlugin: ConfigPlugin<PluginConfig> = (config, pluginConfig
|
|
|
49
60
|
createFileFromTemplate('proguard-rules.pro', brownfieldPath);
|
|
50
61
|
createFileFromTemplate('consumer-rules.pro', brownfieldPath);
|
|
51
62
|
|
|
63
|
+
// Emit the fused siblings' build.gradle.kts files. Same template at
|
|
64
|
+
// packages/expo-brownfield/plugin/templates/android/fused/build.gradle.kts —
|
|
65
|
+
// `fusedVariant` is the only difference: substituted as "release" or "debug",
|
|
66
|
+
// it drives the namespace suffix, publication name, and which build type the
|
|
67
|
+
// sibling resolves and fuses.
|
|
68
|
+
const fusedTemplate = path.join('fused', 'build.gradle.kts');
|
|
69
|
+
const fusedBaseVars = {
|
|
70
|
+
packageId: pluginConfig.package,
|
|
71
|
+
groupId: pluginConfig.group,
|
|
72
|
+
version: pluginConfig.version,
|
|
73
|
+
libraryName: pluginConfig.libraryName,
|
|
74
|
+
};
|
|
75
|
+
createFileFromTemplateAs(fusedTemplate, fusedReleasePath, 'build.gradle.kts', {
|
|
76
|
+
...fusedBaseVars,
|
|
77
|
+
fusedVariant: 'release',
|
|
78
|
+
});
|
|
79
|
+
createFileFromTemplateAs(fusedTemplate, fusedDebugPath, 'build.gradle.kts', {
|
|
80
|
+
...fusedBaseVars,
|
|
81
|
+
fusedVariant: 'debug',
|
|
82
|
+
});
|
|
83
|
+
|
|
52
84
|
// Adjust ReactNativeHostManager and BrownfieldActivity to initialize dev menu
|
|
53
85
|
if (checkPlugin(config, 'expo-dev-menu')) {
|
|
54
86
|
applyPatchToFile(
|
|
@@ -4,14 +4,32 @@ import type { PluginConfig } from '../types';
|
|
|
4
4
|
|
|
5
5
|
const withSettingsGradlePlugin: ConfigPlugin<PluginConfig> = (config, pluginConfig) => {
|
|
6
6
|
return withSettingsGradle(config, (config) => {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Append line-by-line so re-running prebuild without --clean (or after
|
|
8
|
+
// upgrading from a version without the fused siblings) adds only what's
|
|
9
|
+
// missing instead of duplicating the whole block.
|
|
10
|
+
for (const line of getBrownfieldIncludeStatements(pluginConfig.libraryName)) {
|
|
11
|
+
if (!config.modResults.contents.includes(line)) {
|
|
12
|
+
config.modResults.contents += `${line}\n`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!config.modResults.contents.includes('includeBuild(brownfieldPluginsPath)')) {
|
|
16
|
+
config.modResults.contents += getBrownfieldPluginIncludeStatement();
|
|
17
|
+
}
|
|
9
18
|
return config;
|
|
10
19
|
});
|
|
11
20
|
};
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
// Two fused sibling Gradle subprojects (sourceless, applies `com.android.fused-library`)
|
|
23
|
+
// are always emitted — one per variant — so each can be targeted on demand by
|
|
24
|
+
// `expo-brownfield build:android --fused --release` / `--debug` / `--all`. Their
|
|
25
|
+
// build scripts are inert unless the CLI passes `-Pbrownfield.fused=true`, so in
|
|
26
|
+
// default mode they register no publications and resolve nothing.
|
|
27
|
+
const getBrownfieldIncludeStatements = (libraryName: string) => {
|
|
28
|
+
return [
|
|
29
|
+
`include ':${libraryName}'`,
|
|
30
|
+
`include ':${libraryName}-fused-release'`,
|
|
31
|
+
`include ':${libraryName}-fused-debug'`,
|
|
32
|
+
];
|
|
15
33
|
};
|
|
16
34
|
|
|
17
35
|
const getBrownfieldPluginIncludeStatement = () => {
|
|
@@ -15,6 +15,11 @@ const interpolateVariables = (str: string, variables: Record<string, unknown>):
|
|
|
15
15
|
let match = variableRegex.exec(str);
|
|
16
16
|
while (match) {
|
|
17
17
|
const variable = match[0].slice(3, -2);
|
|
18
|
+
if (!(variable in variables)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`The expo-brownfield template variable "${variable}" has no value, so the generated file would contain the literal string "undefined" and fail to build. This usually means the plugin passed an incomplete variable set for this template — report this at https://github.com/expo/expo/issues.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
18
23
|
str = str.replace(match[0], String(variables[variable]));
|
|
19
24
|
match = variableRegex.exec(str);
|
|
20
25
|
}
|