@callstack/react-native-brownfield 5.1.1 → 5.3.0
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 +25 -0
- package/gradle-plugin/brownfield/build.gradle.kts +67 -2
- package/gradle-plugin/brownfield/gradle/libs.versions.toml +5 -2
- package/gradle-plugin/brownfield/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +35 -4
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +6 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +2 -2
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +155 -0
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
- package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/lib/commonjs/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- package/lib/commonjs/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
- package/lib/commonjs/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scaffold/index.js +2 -0
- package/lib/commonjs/scaffold/index.js.map +1 -0
- package/lib/commonjs/scaffold/projectFiles.js +2 -0
- package/lib/commonjs/scaffold/projectFiles.js.map +1 -0
- package/lib/commonjs/scaffold/xcode.d.js +2 -0
- package/lib/commonjs/scaffold/xcode.d.js.map +1 -0
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js +1 -1
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
- package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
- package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/lib/module/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- package/lib/module/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
- package/lib/module/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/scaffold/index.js +2 -0
- package/lib/module/scaffold/index.js.map +1 -0
- package/lib/module/scaffold/projectFiles.js +2 -0
- package/lib/module/scaffold/projectFiles.js.map +1 -0
- package/lib/module/scaffold/xcode.d.js +2 -0
- package/lib/module/scaffold/xcode.d.js.map +1 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +19 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +11 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/index.d.ts +23 -0
- package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +63 -0
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -0
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +19 -1
- package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
- package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +11 -2
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/index.d.ts +23 -0
- package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -0
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts +63 -0
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -0
- package/package.json +16 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +62 -5
- package/src/expo-config-plugin/ios/podfileHelpers.ts +24 -6
- package/src/expo-config-plugin/ios/withBrownfieldIos.ts +5 -2
- package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +21 -6
- package/src/expo-config-plugin/ios/xcodeHelpers.ts +77 -5
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
- package/src/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/src/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- package/src/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
- package/src/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
- package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +3 -2
- package/src/index.ts +7 -0
- package/src/scaffold/index.ts +395 -0
- package/src/scaffold/projectFiles.ts +243 -0
- package/src/scaffold/xcode.d.ts +4 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// Emitted as a triple-slash reference into the generated scaffold .d.ts, so
|
|
2
|
+
// consumers (e.g. @callstack/create-react-native-brownfield) resolve the untyped `xcode`
|
|
3
|
+
// import through this single declaration instead of duplicating xcode.d.ts.
|
|
4
|
+
/// <reference path="./xcode.d.ts" />
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
import type { ModProps, XcodeProject } from '@expo/config-plugins';
|
|
10
|
+
import type { UserConfig } from '@react-native-community/cli-types';
|
|
11
|
+
import cliConfigImport from '@react-native-community/cli-config';
|
|
12
|
+
import xcode from 'xcode';
|
|
13
|
+
|
|
14
|
+
import { Logger } from '../expo-config-plugin/logging';
|
|
15
|
+
import type {
|
|
16
|
+
ResolvedBrownfieldPluginConfigWithAndroid,
|
|
17
|
+
ResolvedBrownfieldPluginConfigWithIos,
|
|
18
|
+
} from '../expo-config-plugin/types';
|
|
19
|
+
import {
|
|
20
|
+
modifyRootBuildGradle,
|
|
21
|
+
modifySettingsGradle,
|
|
22
|
+
} from '../expo-config-plugin/android/utils/gradleHelpers';
|
|
23
|
+
import { createAndroidModule } from '../expo-config-plugin/android/withAndroidModuleFiles';
|
|
24
|
+
import { modifyPodfile } from '../expo-config-plugin/ios/podfileHelpers';
|
|
25
|
+
import {
|
|
26
|
+
addFrameworkTarget,
|
|
27
|
+
addSourceFilesBuildPhase,
|
|
28
|
+
copyBundleReactNativePhase,
|
|
29
|
+
} from '../expo-config-plugin/ios/xcodeHelpers';
|
|
30
|
+
import { createIosFramework } from '../expo-config-plugin/ios/withIosFrameworkFiles';
|
|
31
|
+
import {
|
|
32
|
+
addBrownfieldDependencies,
|
|
33
|
+
addBrownfieldPackageScripts,
|
|
34
|
+
writeBrownfieldFileConfig,
|
|
35
|
+
} from './projectFiles';
|
|
36
|
+
|
|
37
|
+
// ESM/CJS interop shim: `@react-native-community/cli-config` ships CJS whose
|
|
38
|
+
// default export is the function itself, but depending on how this module is
|
|
39
|
+
// loaded (bundler/ESM interop) the import may be the module namespace object
|
|
40
|
+
// with the function on `.default`. Unwrap to the callable either way —
|
|
41
|
+
// do not remove, otherwise the scaffold crashes with "cliConfig is not a function".
|
|
42
|
+
const cliConfig: typeof cliConfigImport =
|
|
43
|
+
typeof cliConfigImport === 'function'
|
|
44
|
+
? cliConfigImport
|
|
45
|
+
: (cliConfigImport as any).default;
|
|
46
|
+
|
|
47
|
+
export type BrownfieldScaffoldOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* React Native project root directory (contains package.json).
|
|
50
|
+
* Defaults to current working directory.
|
|
51
|
+
*/
|
|
52
|
+
projectRoot?: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* iOS framework target name (also framework directory name).
|
|
56
|
+
* Defaults to "BrownfieldLib".
|
|
57
|
+
*/
|
|
58
|
+
iosFrameworkName?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Android library module folder / Gradle module name.
|
|
62
|
+
* Defaults to "brownfieldlib".
|
|
63
|
+
*/
|
|
64
|
+
androidModuleName?: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enables verbose logging.
|
|
68
|
+
*/
|
|
69
|
+
debug?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function findProjectRoot(startDir: string): string {
|
|
73
|
+
let currentDir = startDir;
|
|
74
|
+
while (true) {
|
|
75
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
76
|
+
return currentDir;
|
|
77
|
+
}
|
|
78
|
+
const parent = path.dirname(currentDir);
|
|
79
|
+
if (parent === currentDir) break;
|
|
80
|
+
currentDir = parent;
|
|
81
|
+
}
|
|
82
|
+
throw new Error('Could not find project root (no package.json found)');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveUserConfig(projectRoot: string): UserConfig {
|
|
86
|
+
// Do not pass selectedPlatform: it restricts detection to a single platform
|
|
87
|
+
// and would leave project.android undefined.
|
|
88
|
+
return cliConfig({
|
|
89
|
+
projectRoot,
|
|
90
|
+
}) as UserConfig;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readFileIfExists(filePath: string): string | null {
|
|
94
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeFileIfChanged(filePath: string, next: string) {
|
|
98
|
+
const prev = readFileIfExists(filePath);
|
|
99
|
+
if (prev === next) return;
|
|
100
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function firstXcodeprojPath(iosDir: string): string {
|
|
104
|
+
const entries = fs.readdirSync(iosDir, { withFileTypes: true });
|
|
105
|
+
const xcodeproj = entries.find(
|
|
106
|
+
(e) => e.isDirectory() && e.name.endsWith('.xcodeproj')
|
|
107
|
+
);
|
|
108
|
+
if (!xcodeproj) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Could not find an .xcodeproj under ${iosDir}. Did you run iOS project generation?`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return path.join(iosDir, xcodeproj.name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function unquote(value: string): string {
|
|
117
|
+
return value.replace(/^"+|"+$/g, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveIosAppBundleId(pbxproj: any): string | null {
|
|
121
|
+
const nativeTargets = pbxproj.pbxNativeTargetSection?.() ?? {};
|
|
122
|
+
const configLists = pbxproj.pbxXCConfigurationList?.() ?? {};
|
|
123
|
+
const buildConfigs = pbxproj.pbxXCBuildConfigurationSection?.() ?? {};
|
|
124
|
+
|
|
125
|
+
for (const [key, target] of Object.entries<any>(nativeTargets)) {
|
|
126
|
+
if (key.endsWith('_comment')) continue;
|
|
127
|
+
// heuristic: application targets usually have productType including "application"
|
|
128
|
+
if (
|
|
129
|
+
typeof target?.productType === 'string' &&
|
|
130
|
+
!target.productType.includes('application')
|
|
131
|
+
) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const configListId = target?.buildConfigurationList;
|
|
136
|
+
const configList = configLists?.[configListId];
|
|
137
|
+
const debugConfigId = configList?.buildConfigurations?.find?.(
|
|
138
|
+
(c: any) => c?.comment === 'Debug'
|
|
139
|
+
)?.value;
|
|
140
|
+
const debugConfig = debugConfigId ? buildConfigs?.[debugConfigId] : null;
|
|
141
|
+
const bundleId = debugConfig?.buildSettings?.PRODUCT_BUNDLE_IDENTIFIER;
|
|
142
|
+
if (typeof bundleId === 'string' && bundleId.length > 0) {
|
|
143
|
+
return unquote(bundleId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveReactNativeVersion(projectRoot: string): string {
|
|
151
|
+
// The shipped build is CommonJS, where bare `require` works. If this module
|
|
152
|
+
// is ever loaded as true ESM, bare `require` is undefined; fall back to an
|
|
153
|
+
// explicit `createRequire`. Anchored at cwd (not `import.meta.url`) because
|
|
154
|
+
// Babel keeps `import.meta` verbatim in the CJS output, which is a parse
|
|
155
|
+
// SyntaxError there.
|
|
156
|
+
const localRequire: NodeRequire =
|
|
157
|
+
typeof require === 'undefined'
|
|
158
|
+
? createRequire(path.join(process.cwd(), 'noop.js'))
|
|
159
|
+
: require;
|
|
160
|
+
const rnPkgPath = localRequire.resolve('react-native/package.json', {
|
|
161
|
+
paths: [projectRoot],
|
|
162
|
+
});
|
|
163
|
+
const rnPkg = localRequire(rnPkgPath);
|
|
164
|
+
if (!rnPkg?.version) {
|
|
165
|
+
throw new Error('Could not resolve react-native version from package.json');
|
|
166
|
+
}
|
|
167
|
+
return rnPkg.version;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function scaffoldBrownfieldInRncCliProject(
|
|
171
|
+
options: BrownfieldScaffoldOptions = {}
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const projectRoot = findProjectRoot(
|
|
174
|
+
path.resolve(options.projectRoot ?? process.cwd())
|
|
175
|
+
);
|
|
176
|
+
Logger.setIsDebug(options.debug ?? false);
|
|
177
|
+
|
|
178
|
+
const userConfig = resolveUserConfig(projectRoot);
|
|
179
|
+
const android = userConfig.project.android;
|
|
180
|
+
const ios = userConfig.project.ios;
|
|
181
|
+
|
|
182
|
+
if (!android) {
|
|
183
|
+
throw new Error('Android project not found.');
|
|
184
|
+
}
|
|
185
|
+
if (!ios) {
|
|
186
|
+
throw new Error('iOS project not found.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const androidDir = path.isAbsolute(android.sourceDir)
|
|
190
|
+
? android.sourceDir
|
|
191
|
+
: path.join(projectRoot, android.sourceDir || 'android');
|
|
192
|
+
// Honour the CLI-resolved iOS source directory (symmetry with Android
|
|
193
|
+
// above): a project that sets project.ios.sourceDir to a custom relative
|
|
194
|
+
// path makes the Community CLI resolve it to an absolute directory, and
|
|
195
|
+
// hard-coding <root>/ios would then miss the .xcodeproj or mutate the
|
|
196
|
+
// wrong directory. Note: an absolute sourceDir authored by the user is
|
|
197
|
+
// already rejected upstream (cli-config-apple joins it onto the project
|
|
198
|
+
// root), so the isAbsolute branch mainly guards the resolved value.
|
|
199
|
+
const iosSourceDir = ios.sourceDir;
|
|
200
|
+
const iosDir = path.isAbsolute(iosSourceDir)
|
|
201
|
+
? iosSourceDir
|
|
202
|
+
: path.join(projectRoot, iosSourceDir || 'ios');
|
|
203
|
+
|
|
204
|
+
const rnVersion = resolveReactNativeVersion(projectRoot);
|
|
205
|
+
|
|
206
|
+
const iosFrameworkName = options.iosFrameworkName ?? 'BrownfieldLib';
|
|
207
|
+
const androidModuleName = options.androidModuleName ?? 'brownfieldlib';
|
|
208
|
+
const androidPackageName = android.packageName ?? android.applicationId;
|
|
209
|
+
if (!androidPackageName) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'Could not resolve Android package name from React Native CLI config.'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Tracks the current mutation phase so a mid-way failure can name the step
|
|
216
|
+
// that failed and point users at fix-forward guidance (see catch below).
|
|
217
|
+
let currentStep = 'preconditions';
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
currentStep = 'android-gradle';
|
|
221
|
+
// --- Android: root build.gradle + settings.gradle ---
|
|
222
|
+
const rootBuildGradlePath = path.join(androidDir, 'build.gradle');
|
|
223
|
+
const rootBuildGradle = readFileIfExists(rootBuildGradlePath);
|
|
224
|
+
if (!rootBuildGradle) {
|
|
225
|
+
throw new Error(`Missing ${rootBuildGradlePath}`);
|
|
226
|
+
}
|
|
227
|
+
writeFileIfChanged(
|
|
228
|
+
rootBuildGradlePath,
|
|
229
|
+
modifyRootBuildGradle(rootBuildGradle)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const settingsGradlePath = path.join(androidDir, 'settings.gradle');
|
|
233
|
+
const settingsGradle = readFileIfExists(settingsGradlePath);
|
|
234
|
+
if (!settingsGradle) {
|
|
235
|
+
throw new Error(`Missing ${settingsGradlePath}`);
|
|
236
|
+
}
|
|
237
|
+
writeFileIfChanged(
|
|
238
|
+
settingsGradlePath,
|
|
239
|
+
modifySettingsGradle(settingsGradle, androidModuleName)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const resolvedAndroidConfig: ResolvedBrownfieldPluginConfigWithAndroid = {
|
|
243
|
+
android: {
|
|
244
|
+
moduleName: androidModuleName,
|
|
245
|
+
packageName: androidPackageName,
|
|
246
|
+
// minSdk has no rootProject fallback in the shared build.gradle.kts
|
|
247
|
+
// template (it renders a literal), so keep the Expo plugin default.
|
|
248
|
+
// RN CLI rootProject.ext.minSdkVersion is 24 on current templates
|
|
249
|
+
// anyway; the generated build.gradle.kts stays readable.
|
|
250
|
+
minSdkVersion: 24,
|
|
251
|
+
// compile/target SDK are intentionally not hardcoded: leaving them
|
|
252
|
+
// unset makes the shared template fall back to
|
|
253
|
+
// resolveRootProjectInt("compileSdkVersion"/"targetSdkVersion"), i.e.
|
|
254
|
+
// the app's own rootProject.ext values (RN 0.87 needs compileSdk 37+;
|
|
255
|
+
// AGP 9 rejects library `targetSdk` entirely — see
|
|
256
|
+
// renderTargetSdkBlock). Proven against a real RN 0.87.1 app.
|
|
257
|
+
targetSdkVersion: undefined,
|
|
258
|
+
compileSdkVersion: undefined,
|
|
259
|
+
groupId: androidPackageName,
|
|
260
|
+
artifactId: androidModuleName,
|
|
261
|
+
version: '0.0.1-SNAPSHOT',
|
|
262
|
+
// Fields added to the resolved config after the original draft was
|
|
263
|
+
// written (BGP local-plugin wiring + flavor dimensions, #448/#458).
|
|
264
|
+
// Defaults match the Expo plugin defaults for RN CLI projects.
|
|
265
|
+
useLocalGradlePlugin: false,
|
|
266
|
+
useLocalMaven: false,
|
|
267
|
+
missingDimensionStrategies: [],
|
|
268
|
+
},
|
|
269
|
+
ios: null,
|
|
270
|
+
debug: options.debug ?? false,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
currentStep = 'android-module';
|
|
274
|
+
createAndroidModule({
|
|
275
|
+
androidDir,
|
|
276
|
+
config: resolvedAndroidConfig,
|
|
277
|
+
rnVersion,
|
|
278
|
+
// 'vanilla' = non-Expo host (same axis as `useExpoHost: false` below).
|
|
279
|
+
templateVariant: 'vanilla',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// --- iOS: xcodeproj + Podfile + framework source files ---
|
|
283
|
+
currentStep = 'ios-project';
|
|
284
|
+
const xcodeprojPath = firstXcodeprojPath(iosDir);
|
|
285
|
+
const pbxprojPath = path.join(xcodeprojPath, 'project.pbxproj');
|
|
286
|
+
if (!fs.existsSync(pbxprojPath)) {
|
|
287
|
+
throw new Error(`Missing ${pbxprojPath}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const project = xcode.project(pbxprojPath);
|
|
291
|
+
project.parseSync();
|
|
292
|
+
|
|
293
|
+
const appBundleId = resolveIosAppBundleId(project);
|
|
294
|
+
const brownfieldBundleId = appBundleId
|
|
295
|
+
? `${appBundleId}.brownfield`
|
|
296
|
+
: `com.brownfield.${iosFrameworkName.toLowerCase()}`;
|
|
297
|
+
|
|
298
|
+
const resolvedIosConfig: ResolvedBrownfieldPluginConfigWithIos = {
|
|
299
|
+
ios: {
|
|
300
|
+
frameworkName: iosFrameworkName,
|
|
301
|
+
bundleIdentifier: brownfieldBundleId,
|
|
302
|
+
buildSettings: {},
|
|
303
|
+
// Defaults aligned with the Expo plugin defaults (Expo's fallback
|
|
304
|
+
// when no deployment target can be derived; see
|
|
305
|
+
// resolveFrameworkDeploymentTarget). RN CLI config does not expose
|
|
306
|
+
// an iOS deployment target directly.
|
|
307
|
+
// TODO: derive from the Podfile `platform :ios` line or RN's
|
|
308
|
+
// min_ios_version_supported, or expose a flag.
|
|
309
|
+
deploymentTarget: '15.0',
|
|
310
|
+
// Standard CFBundleShortVersionString for the generated framework
|
|
311
|
+
// (matches the Expo plugin default); unrelated to package versioning.
|
|
312
|
+
frameworkVersion: '1',
|
|
313
|
+
},
|
|
314
|
+
android: null,
|
|
315
|
+
debug: options.debug ?? false,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Contract: downstream helpers (xcodeHelpers.addFrameworkTarget /
|
|
319
|
+
// resolveAppTargetName) only read `platformProjectRoot` (framework group
|
|
320
|
+
// location) and `projectName` (app-target-name fallback), but the shared
|
|
321
|
+
// Expo-path signatures require a full ModProps, so we provide every field
|
|
322
|
+
// with scaffold-appropriate values (no mods are run here, so `introspect`
|
|
323
|
+
// is false and `nextMod` is omitted).
|
|
324
|
+
const modRequest: ModProps<XcodeProject> = {
|
|
325
|
+
projectRoot,
|
|
326
|
+
platformProjectRoot: iosDir,
|
|
327
|
+
modName: 'react-native-brownfield-scaffold',
|
|
328
|
+
platform: 'ios',
|
|
329
|
+
introspect: false,
|
|
330
|
+
projectName: path.basename(xcodeprojPath, '.xcodeproj'),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const { frameworkTargetUUID } = addFrameworkTarget(
|
|
334
|
+
project,
|
|
335
|
+
modRequest,
|
|
336
|
+
resolvedIosConfig.ios,
|
|
337
|
+
{ useExpoHost: false }
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
copyBundleReactNativePhase(project, frameworkTargetUUID);
|
|
341
|
+
addSourceFilesBuildPhase(
|
|
342
|
+
project,
|
|
343
|
+
frameworkTargetUUID,
|
|
344
|
+
resolvedIosConfig.ios,
|
|
345
|
+
{ useExpoHost: false }
|
|
346
|
+
);
|
|
347
|
+
// xcode@3.x writeSync() returns the serialized project but does not write it.
|
|
348
|
+
// This is the riskiest write: a failure here leaves a partially mutated
|
|
349
|
+
// pbxproj on disk (the in-memory changes are lost, not half-written).
|
|
350
|
+
fs.writeFileSync(pbxprojPath, project.writeSync());
|
|
351
|
+
|
|
352
|
+
currentStep = 'ios-podfile';
|
|
353
|
+
const podfilePath = path.join(iosDir, 'Podfile');
|
|
354
|
+
const podfile = readFileIfExists(podfilePath);
|
|
355
|
+
if (!podfile) {
|
|
356
|
+
throw new Error(`Missing ${podfilePath}`);
|
|
357
|
+
}
|
|
358
|
+
writeFileIfChanged(podfilePath, modifyPodfile(podfile, iosFrameworkName));
|
|
359
|
+
|
|
360
|
+
currentStep = 'ios-sources';
|
|
361
|
+
createIosFramework(iosDir, resolvedIosConfig, { useExpoHost: false });
|
|
362
|
+
|
|
363
|
+
// --- App package wiring: Brownfield deps + package:* scripts ---
|
|
364
|
+
// The scaffolded Kotlin host imports com.callstack.reactnativebrownfield.*,
|
|
365
|
+
// so the JS/runtime package must be an app dependency for native builds to
|
|
366
|
+
// resolve it. Idempotent: existing version specs are left untouched.
|
|
367
|
+
currentStep = 'package-dependencies';
|
|
368
|
+
addBrownfieldDependencies(projectRoot);
|
|
369
|
+
|
|
370
|
+
currentStep = 'package-scripts';
|
|
371
|
+
addBrownfieldPackageScripts(projectRoot);
|
|
372
|
+
|
|
373
|
+
// --- brownfield.config.json: packaging settings for `brownfield package:*` ---
|
|
374
|
+
// Deterministic content derived from the scaffolded names, so re-runs
|
|
375
|
+
// produce identical bytes (overwrite == no-op).
|
|
376
|
+
currentStep = 'brownfield-config';
|
|
377
|
+
writeBrownfieldFileConfig(projectRoot, {
|
|
378
|
+
iosFrameworkName,
|
|
379
|
+
androidModuleName,
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
383
|
+
Logger.logInfo(
|
|
384
|
+
'Scaffolding failed mid-way; fix-forward: re-running the CLI is idempotent for completed steps (gradle settings, Podfile, xcodeproj target detection, package.json deps/scripts, brownfield.config.json) — inspect partial changes with git diff.'
|
|
385
|
+
);
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Brownfield scaffolding failed during step "${currentStep}": ${message}`,
|
|
388
|
+
{
|
|
389
|
+
cause: error,
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
Logger.logInfo('Brownfield scaffolding complete.');
|
|
395
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '../expo-config-plugin/logging';
|
|
5
|
+
|
|
6
|
+
export const BROWNFIELD_RUNTIME_PACKAGE_NAME =
|
|
7
|
+
'@callstack/react-native-brownfield';
|
|
8
|
+
export const BROWNFIELD_CLI_PACKAGE_NAME = '@callstack/brownfield-cli';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Match the docs convention (getting-started/quick-start + expo.mdx): both
|
|
12
|
+
* packages are regular dependencies. The brownfield CLI ships as a dependency
|
|
13
|
+
* of the runtime package, so a consistent caret range pinned to this package's
|
|
14
|
+
* own published version keeps the two in lockstep (they are released together).
|
|
15
|
+
*/
|
|
16
|
+
export function resolveBrownfieldPackageVersionRange(): string {
|
|
17
|
+
return `^${readOwnPackageVersion()}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readOwnPackageVersion(): string {
|
|
21
|
+
// Walk up from this module to its own package.json. Works both in the
|
|
22
|
+
// published layout (lib/commonjs/scaffold -> package root) and in-repo
|
|
23
|
+
// (src/scaffold -> package root). `__dirname` is absent in true-ESM builds
|
|
24
|
+
// (and `import.meta` must not be used here — Babel keeps it verbatim in the
|
|
25
|
+
// CJS output, which is a parse SyntaxError there; see index.ts), so fall
|
|
26
|
+
// back to cwd, which is correct whenever the package is resolved from the
|
|
27
|
+
// app's own node_modules chain.
|
|
28
|
+
let dir = typeof __dirname === 'string' ? __dirname : process.cwd();
|
|
29
|
+
while (true) {
|
|
30
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
31
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
|
|
33
|
+
name?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
};
|
|
36
|
+
if (pkg.name === BROWNFIELD_RUNTIME_PACKAGE_NAME) {
|
|
37
|
+
if (!pkg.version) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${BROWNFIELD_RUNTIME_PACKAGE_NAME} package.json has no version`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return pkg.version;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const parent = path.dirname(dir);
|
|
46
|
+
if (parent === dir) break;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Could not locate the ${BROWNFIELD_RUNTIME_PACKAGE_NAME} package.json to resolve dependency versions.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type PackageJson = {
|
|
55
|
+
dependencies?: Record<string, string>;
|
|
56
|
+
devDependencies?: Record<string, string>;
|
|
57
|
+
scripts?: Record<string, string>;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function readPackageJson(projectRoot: string): PackageJson {
|
|
62
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
63
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
64
|
+
throw new Error(`Missing ${packageJsonPath}`);
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageJson;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writePackageJson(projectRoot: string, pkg: PackageJson): void {
|
|
70
|
+
// Stable formatting (2-space indent, trailing newline), so repeated runs
|
|
71
|
+
// produce byte-identical output.
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(projectRoot, 'package.json'),
|
|
74
|
+
JSON.stringify(pkg, null, 2) + '\n',
|
|
75
|
+
'utf8'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureDependency(
|
|
80
|
+
pkg: PackageJson,
|
|
81
|
+
packageName: string,
|
|
82
|
+
versionRange: string
|
|
83
|
+
): boolean {
|
|
84
|
+
// Idempotency: if the package is declared anywhere already, leave the
|
|
85
|
+
// user's version spec untouched (same spirit as the Podfile/gradle guards).
|
|
86
|
+
if (pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
90
|
+
pkg.dependencies[packageName] = versionRange;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Adds @callstack/react-native-brownfield (the Kotlin/Swift host code the
|
|
96
|
+
* scaffolded module imports) and @callstack/brownfield-cli (the `brownfield`
|
|
97
|
+
* bin used by the package:* scripts) to the app's dependencies.
|
|
98
|
+
* Existing declarations are never overwritten. Returns the names added.
|
|
99
|
+
*/
|
|
100
|
+
export function addBrownfieldDependencies(projectRoot: string): string[] {
|
|
101
|
+
const pkg = readPackageJson(projectRoot);
|
|
102
|
+
const versionRange = resolveBrownfieldPackageVersionRange();
|
|
103
|
+
|
|
104
|
+
const added: string[] = [];
|
|
105
|
+
if (ensureDependency(pkg, BROWNFIELD_RUNTIME_PACKAGE_NAME, versionRange)) {
|
|
106
|
+
added.push(BROWNFIELD_RUNTIME_PACKAGE_NAME);
|
|
107
|
+
}
|
|
108
|
+
if (ensureDependency(pkg, BROWNFIELD_CLI_PACKAGE_NAME, versionRange)) {
|
|
109
|
+
added.push(BROWNFIELD_CLI_PACKAGE_NAME);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (added.length > 0) {
|
|
113
|
+
writePackageJson(projectRoot, pkg);
|
|
114
|
+
Logger.logDebug(
|
|
115
|
+
`Added Brownfield dependencies: ${added.join(', ')} (${versionRange})`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return added;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adds the `package:ios` / `package:android` scripts from the docs' manual
|
|
123
|
+
* steps (settings come from the generated brownfield.config.json, so no flags
|
|
124
|
+
* are duplicated here). Existing scripts with the same names are kept.
|
|
125
|
+
*/
|
|
126
|
+
export function addBrownfieldPackageScripts(projectRoot: string): string[] {
|
|
127
|
+
const pkg = readPackageJson(projectRoot);
|
|
128
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
129
|
+
|
|
130
|
+
const desiredScripts: Record<string, string> = {
|
|
131
|
+
'package:ios': 'brownfield package:ios',
|
|
132
|
+
'package:android': 'brownfield package:android',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const added: string[] = [];
|
|
136
|
+
for (const [name, command] of Object.entries(desiredScripts)) {
|
|
137
|
+
if (pkg.scripts[name] === undefined) {
|
|
138
|
+
pkg.scripts[name] = command;
|
|
139
|
+
added.push(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (added.length > 0) {
|
|
144
|
+
writePackageJson(projectRoot, pkg);
|
|
145
|
+
Logger.logDebug(`Added Brownfield scripts: ${added.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
return added;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type BrownfieldFileConfig = {
|
|
151
|
+
$schema: string;
|
|
152
|
+
ios: {
|
|
153
|
+
scheme: string;
|
|
154
|
+
configuration: string;
|
|
155
|
+
};
|
|
156
|
+
android: {
|
|
157
|
+
moduleName: string;
|
|
158
|
+
variant: string;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds the brownfield.config.json content for the scaffolded targets,
|
|
164
|
+
* matching the manual steps in docs getting-started/ios.mdx (step 5) and
|
|
165
|
+
* android.mdx (step 7) and the CLI config schema (packages/cli/src/types.ts,
|
|
166
|
+
* strict additionalProperties: false).
|
|
167
|
+
*
|
|
168
|
+
* Idempotency strategy: the content is derived deterministically from the
|
|
169
|
+
* scaffolded project state (framework/module names), so re-running the
|
|
170
|
+
* scaffold overwrites with byte-identical content. Users customizing it are
|
|
171
|
+
* taking over responsibility; a re-scaffold with the same names is a no-op.
|
|
172
|
+
*/
|
|
173
|
+
export function createBrownfieldFileConfig({
|
|
174
|
+
iosFrameworkName,
|
|
175
|
+
androidModuleName,
|
|
176
|
+
}: {
|
|
177
|
+
iosFrameworkName: string;
|
|
178
|
+
androidModuleName: string;
|
|
179
|
+
}): BrownfieldFileConfig {
|
|
180
|
+
return {
|
|
181
|
+
$schema: 'https://oss.callstack.com/react-native-brownfield/schema.json',
|
|
182
|
+
ios: {
|
|
183
|
+
// iOS packaging builds the generated framework target by scheme name.
|
|
184
|
+
scheme: iosFrameworkName,
|
|
185
|
+
configuration: 'Release',
|
|
186
|
+
},
|
|
187
|
+
android: {
|
|
188
|
+
// Gradle module name of the generated library module.
|
|
189
|
+
moduleName: androidModuleName,
|
|
190
|
+
variant: 'Release',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Writes brownfield.config.json at the project root. Deterministic output:
|
|
197
|
+
* same inputs -> same bytes, so re-running the scaffold never duplicates or
|
|
198
|
+
* drifts the file.
|
|
199
|
+
*
|
|
200
|
+
* Skips writing (returns null) when another Brownfield config source already
|
|
201
|
+
* exists: packages/cli loadBrownfieldConfig fails hard on multiple config
|
|
202
|
+
* files, so an existing brownfield.config.js or a legacy `brownfield` key in
|
|
203
|
+
* package.json takes precedence over generating a JSON one.
|
|
204
|
+
*/
|
|
205
|
+
export function writeBrownfieldFileConfig(
|
|
206
|
+
projectRoot: string,
|
|
207
|
+
names: { iosFrameworkName: string; androidModuleName: string }
|
|
208
|
+
): string | null {
|
|
209
|
+
const configPath = path.join(projectRoot, 'brownfield.config.json');
|
|
210
|
+
|
|
211
|
+
const jsConfigPath = path.join(projectRoot, 'brownfield.config.js');
|
|
212
|
+
if (fs.existsSync(jsConfigPath)) {
|
|
213
|
+
Logger.logDebug(
|
|
214
|
+
`Skipping brownfield.config.json: ${jsConfigPath} already exists.`
|
|
215
|
+
);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const pkg = readPackageJson(projectRoot);
|
|
220
|
+
if ('brownfield' in pkg) {
|
|
221
|
+
Logger.logDebug(
|
|
222
|
+
'Skipping brownfield.config.json: legacy "brownfield" key in package.json already exists.'
|
|
223
|
+
);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Trailing newline, same as writePackageJson: the idempotency guard below
|
|
228
|
+
// compares bytes, so the written content must be exactly what a
|
|
229
|
+
// newline-normalizing toolchain (editorconfig, prettier, git text eol)
|
|
230
|
+
// leaves on disk. Content + '\n' is both the written bytes and the
|
|
231
|
+
// comparison value, so the guard cannot be broken by an external normalizer.
|
|
232
|
+
const content =
|
|
233
|
+
JSON.stringify(createBrownfieldFileConfig(names), null, 2) + '\n';
|
|
234
|
+
const prev = fs.existsSync(configPath)
|
|
235
|
+
? fs.readFileSync(configPath, 'utf8')
|
|
236
|
+
: null;
|
|
237
|
+
|
|
238
|
+
if (prev !== content) {
|
|
239
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
240
|
+
Logger.logDebug(`Wrote ${configPath}`);
|
|
241
|
+
}
|
|
242
|
+
return configPath;
|
|
243
|
+
}
|