@callstack/react-native-brownfield 5.2.0 → 6.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/android/build.gradle +1 -0
  3. package/android/consumer-rules.pro +6 -0
  4. package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformance.kt +384 -0
  5. package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformanceModule.kt +15 -0
  6. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt +8 -5
  7. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +78 -23
  8. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldPackage.kt +1 -0
  9. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +89 -3
  10. package/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt +2 -0
  11. package/android/src/test/java/com/callstack/reactnativebrownfield/PerformanceMetricsTest.kt +54 -0
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +59 -15
  13. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +6 -8
  14. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +6 -4
  15. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/RNBundleTaskStubs.kt +27 -0
  16. package/ios/BrownfieldDisplayMetrics.swift +265 -0
  17. package/ios/BrownfieldPerformanceBridge.h +8 -0
  18. package/ios/BrownfieldPerformanceBridge.mm +103 -0
  19. package/ios/Expo/ExpoHostRuntime.swift +58 -14
  20. package/ios/Expo/ExpoUpdatesStartup.swift +49 -0
  21. package/ios/JSBundleLoadObserver.swift +56 -19
  22. package/ios/JSBundleTimingObserver.h +26 -0
  23. package/ios/JSBundleTimingObserver.m +140 -0
  24. package/ios/ReactHostPreloading.swift +7 -3
  25. package/ios/ReactNativeBrownfield.swift +82 -14
  26. package/ios/ReactNativeBrownfield.xcodeproj/project.pbxproj +16 -0
  27. package/ios/Vanilla/ReactNativeHostRuntime.swift +1 -1
  28. package/ios/Views/ReactNativeView.swift +21 -3
  29. package/ios/Views/ReactNativeViewController.swift +44 -42
  30. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  31. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  32. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +1 -1
  33. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  34. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  35. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  36. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  37. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  38. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  39. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  40. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  41. package/lib/commonjs/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  42. package/lib/commonjs/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  43. package/lib/commonjs/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  44. package/lib/commonjs/index.js +1 -1
  45. package/lib/commonjs/index.js.map +1 -1
  46. package/lib/commonjs/scaffold/index.js +2 -0
  47. package/lib/commonjs/scaffold/index.js.map +1 -0
  48. package/lib/commonjs/scaffold/projectFiles.js +2 -0
  49. package/lib/commonjs/scaffold/projectFiles.js.map +1 -0
  50. package/lib/commonjs/scaffold/xcode.d.js +2 -0
  51. package/lib/commonjs/scaffold/xcode.d.js.map +1 -0
  52. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  53. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  54. package/lib/module/expo-config-plugin/ios/podfileHelpers.js +1 -1
  55. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  56. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  57. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  58. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  59. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  60. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  61. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  62. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  63. package/lib/module/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  64. package/lib/module/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  65. package/lib/module/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  66. package/lib/module/index.js +1 -1
  67. package/lib/module/index.js.map +1 -1
  68. package/lib/module/scaffold/index.js +2 -0
  69. package/lib/module/scaffold/index.js.map +1 -0
  70. package/lib/module/scaffold/projectFiles.js +2 -0
  71. package/lib/module/scaffold/projectFiles.js.map +1 -0
  72. package/lib/module/scaffold/xcode.d.js +2 -0
  73. package/lib/module/scaffold/xcode.d.js.map +1 -0
  74. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +10 -2
  75. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
  77. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +11 -2
  80. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
  82. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/src/index.d.ts +7 -0
  84. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/src/scaffold/index.d.ts +23 -0
  86. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +63 -0
  88. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -0
  89. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +10 -2
  90. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  91. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
  92. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  93. package/lib/typescript/module/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
  94. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +11 -2
  95. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  96. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
  97. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  98. package/lib/typescript/module/src/index.d.ts +7 -0
  99. package/lib/typescript/module/src/index.d.ts.map +1 -1
  100. package/lib/typescript/module/src/scaffold/index.d.ts +23 -0
  101. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -0
  102. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +63 -0
  103. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -0
  104. package/package.json +16 -2
  105. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +33 -5
  106. package/src/expo-config-plugin/ios/podfileHelpers.ts +24 -6
  107. package/src/expo-config-plugin/ios/withBrownfieldIos.ts +5 -2
  108. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +21 -6
  109. package/src/expo-config-plugin/ios/xcodeHelpers.ts +77 -5
  110. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  111. package/src/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  112. package/src/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  113. package/src/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  114. package/src/index.ts +20 -1
  115. package/src/scaffold/index.ts +395 -0
  116. package/src/scaffold/projectFiles.ts +243 -0
  117. package/src/scaffold/xcode.d.ts +4 -0
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'xcode' {
2
+ const xcode: any;
3
+ export default xcode;
4
+ }