@granite-js/screen 1.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 (99) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/GraniteScreen.podspec +25 -0
  3. package/LICENSE +202 -0
  4. package/android/CMakeLists.txt +62 -0
  5. package/android/build.gradle +63 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/BundleEvaluator.cpp +27 -0
  8. package/android/src/main/cpp/BundleEvaluator.h +17 -0
  9. package/android/src/main/cpp/onLoad.cpp +6 -0
  10. package/android/src/main/kotlin/run/granite/BundleEvaluator.kt +50 -0
  11. package/android/src/main/kotlin/run/granite/BundleLoader.kt +40 -0
  12. package/android/src/main/kotlin/run/granite/DefaultBundleLoader.kt +20 -0
  13. package/android/src/main/kotlin/run/granite/DefaultErrorView.kt +58 -0
  14. package/android/src/main/kotlin/run/granite/DefaultLoadingView.kt +51 -0
  15. package/android/src/main/kotlin/run/granite/GraniteReactDelegate.kt +76 -0
  16. package/android/src/main/kotlin/run/granite/GraniteReactDelegateImpl.kt +448 -0
  17. package/android/src/main/kotlin/run/granite/GraniteReactHost.kt +113 -0
  18. package/android/src/main/kotlin/run/granite/ReactHostFactory.kt +106 -0
  19. package/gradle-plugin/LICENSE +201 -0
  20. package/gradle-plugin/README.md +578 -0
  21. package/gradle-plugin/build.gradle.kts +97 -0
  22. package/gradle-plugin/gradle/libs.versions.toml +17 -0
  23. package/gradle-plugin/gradle/wrapper/gradle-wrapper.jar +0 -0
  24. package/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +7 -0
  25. package/gradle-plugin/gradle.properties +12 -0
  26. package/gradle-plugin/gradlew +248 -0
  27. package/gradle-plugin/gradlew.bat +93 -0
  28. package/gradle-plugin/settings.gradle.kts +1 -0
  29. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteExtension.kt +225 -0
  30. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GranitePlugin.kt +784 -0
  31. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootExtension.kt +107 -0
  32. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootProjectPlugin.kt +290 -0
  33. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/BuildConfigConfigurator.kt +69 -0
  34. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyConfigurator.kt +232 -0
  35. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyCoordinates.kt +29 -0
  36. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DevServerResourceConfigurator.kt +101 -0
  37. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/JniPackagingConfigurator.kt +160 -0
  38. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/NdkConfigurator.kt +135 -0
  39. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/RepositoryConfigurator.kt +148 -0
  40. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/ResourceConfigurator.kt +56 -0
  41. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CMakeGenerator.kt +105 -0
  42. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CppAutolinkingGenerator.kt +152 -0
  43. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/EntryPointGenerator.kt +100 -0
  44. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AndroidDependencyConfig.kt +23 -0
  45. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AutolinkingConfig.kt +89 -0
  46. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/CMakeEntry.kt +47 -0
  47. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/NativeModule.kt +177 -0
  48. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AssetPackagingTask.kt +194 -0
  49. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AutolinkingTask.kt +431 -0
  50. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/BundleTask.kt +275 -0
  51. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenArtifactsTask.kt +218 -0
  52. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenSchemaTask.kt +186 -0
  53. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/AutolinkingParser.kt +128 -0
  54. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ConflictDetector.kt +121 -0
  55. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/JdkValidator.kt +73 -0
  56. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/NodeExecutableFinder.kt +43 -0
  57. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ReactNativeVersionReader.kt +329 -0
  58. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/TaskDependencyValidator.kt +198 -0
  59. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteExtensionTest.kt +191 -0
  60. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GranitePluginTest.kt +156 -0
  61. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteRootProjectPluginTest.kt +87 -0
  62. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/BuildConfigConfiguratorTest.kt +115 -0
  63. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DependencyConfiguratorTest.kt +338 -0
  64. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DevServerResourceConfiguratorTest.kt +205 -0
  65. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/ResourceConfiguratorTest.kt +131 -0
  66. package/gradle-plugin/src/test/kotlin/run/granite/gradle/fixtures/NativeModuleFixtures.kt +67 -0
  67. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CMakeGeneratorTest.kt +71 -0
  68. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CppAutolinkingGeneratorTest.kt +344 -0
  69. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/EntryPointGeneratorTest.kt +40 -0
  70. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/AutolinkingConfigTest.kt +350 -0
  71. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/CMakeEntryTest.kt +200 -0
  72. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/NativeModuleTest.kt +562 -0
  73. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AssetPackagingTaskTest.kt +318 -0
  74. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AutolinkingTaskTest.kt +89 -0
  75. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/BundleTaskTest.kt +68 -0
  76. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/CodegenTasksTest.kt +410 -0
  77. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/AutolinkingParserTest.kt +335 -0
  78. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ConflictDetectorTest.kt +75 -0
  79. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/JdkValidatorTest.kt +88 -0
  80. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ReactNativeVersionReaderTest.kt +585 -0
  81. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskDependencyValidatorTest.kt +123 -0
  82. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskTestUtils.kt +88 -0
  83. package/gradle-plugin/src/test/resources/fixtures/sample-rn-config.json +45 -0
  84. package/ios/BundleLoader/BundleEvaluator.h +16 -0
  85. package/ios/BundleLoader/BundleEvaluator.mm +76 -0
  86. package/ios/BundleLoader/BundleLoadable.swift +91 -0
  87. package/ios/GraniteBundleLoaderTypes.swift +7 -0
  88. package/ios/GraniteScreen.h +12 -0
  89. package/ios/ReactNativeHosting/DefaultViews.swift +138 -0
  90. package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.h +24 -0
  91. package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.mm +22 -0
  92. package/ios/ReactNativeHosting/GraniteHostingHelper.swift +103 -0
  93. package/ios/ReactNativeHosting/GraniteNativeFactory.swift +35 -0
  94. package/ios/ReactNativeHosting/GraniteNativeFactoryDelegateImpl.swift +30 -0
  95. package/ios/ReactNativeHosting/GraniteNativeFactoryImpl.swift +24 -0
  96. package/ios/ReactNativeHosting/GraniteReactHost.swift +39 -0
  97. package/ios/ReactNativeHosting/GraniteScreen-Bridging-Header.h +12 -0
  98. package/package.json +59 -0
  99. package/react-native.config.js +8 -0
@@ -0,0 +1,784 @@
1
+ package run.granite.gradle
2
+
3
+ import com.android.build.api.variant.LibraryAndroidComponentsExtension
4
+ import com.android.build.gradle.LibraryExtension
5
+ import org.gradle.api.Action
6
+ import org.gradle.api.JavaVersion
7
+ import org.gradle.api.Plugin
8
+ import org.gradle.api.Project
9
+ import org.gradle.api.tasks.TaskProvider
10
+ import run.granite.gradle.config.BuildConfigConfigurator
11
+ import run.granite.gradle.config.DependencyConfigurator
12
+ import run.granite.gradle.config.DevServerResourceConfigurator
13
+ import run.granite.gradle.config.JniPackagingConfigurator
14
+ import run.granite.gradle.config.NdkConfigurator
15
+ import run.granite.gradle.config.RepositoryConfigurator
16
+ import run.granite.gradle.config.ResourceConfigurator
17
+ import run.granite.gradle.tasks.AssetPackagingTask
18
+ import run.granite.gradle.tasks.AutolinkingTask
19
+ import run.granite.gradle.tasks.BundleTask
20
+ import run.granite.gradle.tasks.CodegenArtifactsTask
21
+ import run.granite.gradle.tasks.CodegenSchemaTask
22
+ import run.granite.gradle.utils.AutolinkingParser
23
+ import run.granite.gradle.utils.ConflictDetector
24
+ import run.granite.gradle.utils.JdkValidator
25
+ import run.granite.gradle.utils.NodeExecutableFinder
26
+ import java.io.ByteArrayOutputStream
27
+ import java.io.File
28
+
29
+ /**
30
+ * Granite Gradle Plugin
31
+ *
32
+ * Enables packaging React Native functionality within Android library modules (AAR files).
33
+ *
34
+ * This plugin automates:
35
+ * - Codegen for TurboModules and Fabric components
36
+ * - Autolinking for native module discovery
37
+ * - Native compilation (CMake/NDK) with Prefab packaging
38
+ * - JavaScript bundling and Hermes bytecode compilation
39
+ *
40
+ * The plugin only applies to Android library modules (not application modules) and
41
+ * enforces that only one library module per dependency tree can use the plugin.
42
+ *
43
+ * Requirements:
44
+ * - JDK 17+
45
+ * - Gradle 8.x+
46
+ * - Android Gradle Plugin 7.x+
47
+ * - React Native 0.81.x+
48
+ * - Hermes JavaScript engine (always enabled, JSC not supported)
49
+ *
50
+ * @see GraniteExtension for configuration options
51
+ */
52
+ class GranitePlugin : Plugin<Project> {
53
+
54
+ companion object {
55
+ const val PLUGIN_ID = "run.granite.library"
56
+ const val EXTENSION_NAME = "granite"
57
+ const val PLUGIN_GROUP = "granite"
58
+ }
59
+
60
+ override fun apply(project: Project) {
61
+ // Validate JDK version
62
+ JdkValidator.validate(project)
63
+
64
+ // Detect plugin conflicts
65
+ ConflictDetector.validateNoConflicts(project)
66
+
67
+ // Validate this is a library module
68
+ validateLibraryModule(project)
69
+
70
+ // Create the granite {} extension
71
+ val extension = project.extensions.create(
72
+ EXTENSION_NAME,
73
+ GraniteExtension::class.java,
74
+ project,
75
+ )
76
+
77
+ // Apply Android Library plugin if not already applied
78
+ project.pluginManager.apply("com.android.library")
79
+
80
+ // Get Android extension for early configuration
81
+ val androidExtension = project.extensions.getByType(LibraryExtension::class.java)
82
+
83
+ // Configure JDK toolchain (Java 17)
84
+ configureJdkToolchain(project, androidExtension)
85
+
86
+ // Configure Java 17 for node_modules projects early (before AGP finalization)
87
+ // Ensure autolinked modules are compiled with Java 17
88
+ configureNodeModulesJavaVersion(project)
89
+
90
+ // Configure NDK/CMake settings early
91
+ // This must be done before afterEvaluate to avoid "too late to set path" error
92
+ val ndkConfigurator = NdkConfigurator(project, extension, androidExtension)
93
+ ndkConfigurator.configure()
94
+
95
+ // Configure resource packaging early
96
+ // This must be done before afterEvaluate to avoid "too late to modify excludes" error
97
+ val resourceConfigurator = ResourceConfigurator(project, extension)
98
+ resourceConfigurator.configure(androidExtension)
99
+
100
+ // Get Android Components Extension for variant-aware configuration
101
+ val androidComponents = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java)
102
+
103
+ // Configure JNI packaging options early (CRITICAL for Hermes)
104
+ // This must be done before variant processing to avoid duplicate .so file conflicts
105
+ val jniPackagingConfigurator = JniPackagingConfigurator(project, extension)
106
+ jniPackagingConfigurator.configure(androidComponents)
107
+
108
+ // CRITICAL: Register generated sources at DSL and variant level BEFORE afterEvaluate
109
+ // This must happen early so finalizeDsl and onVariants callbacks can be registered
110
+ // before the variant processing phase begins
111
+ registerGeneratedSourcesInDsl(project, androidComponents)
112
+ registerGeneratedSourcesInVariants(project, androidComponents)
113
+
114
+ // Register variant-aware tasks BEFORE afterEvaluate
115
+ // This must be done early to avoid "too late to add actions" error
116
+ registerBundleAndAssetTasksEarly(project, extension, androidComponents)
117
+
118
+ // Configure after Android plugin is evaluated
119
+ project.afterEvaluate {
120
+ configurePlugin(project, extension, androidComponents)
121
+ }
122
+ }
123
+
124
+ private fun validateLibraryModule(project: Project) {
125
+ project.pluginManager.withPlugin("com.android.application") {
126
+ error(
127
+ """
128
+ |Granite plugin can only be applied to Android library modules, not application modules.
129
+ |
130
+ |The plugin was applied to '${project.path}', which is an Android application module.
131
+ |
132
+ |Solution: Remove the 'run.granite.library' plugin from application modules.
133
+ |Library modules using React Native should apply this plugin, and app modules
134
+ |should simply add the library as a dependency.
135
+ |
136
+ |Example (library module build.gradle):
137
+ | plugins {
138
+ | id("com.android.library")
139
+ | id("run.granite.library")
140
+ | }
141
+ |
142
+ |Example (app module build.gradle):
143
+ | dependencies {
144
+ | implementation(project(":your-library-module"))
145
+ | }
146
+ """.trimMargin(),
147
+ )
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Configures Java/Kotlin toolchain to Java 17.
153
+ *
154
+ * Equivalent to @react-native/gradle-plugin's JdkConfiguratorUtils.configureJavaToolChains().
155
+ */
156
+ private fun configureJdkToolchain(project: Project, androidExtension: LibraryExtension) {
157
+ // Configure Java 17 source/target compatibility
158
+ androidExtension.compileOptions {
159
+ sourceCompatibility = JavaVersion.VERSION_17
160
+ targetCompatibility = JavaVersion.VERSION_17
161
+ }
162
+
163
+ // Configure Kotlin JVM target
164
+ // Kotlin tasks typically follow Java targetCompatibility automatically,
165
+ // but we explicitly set kotlinOptions.jvmTarget = "17"
166
+ project.tasks.withType(org.gradle.api.tasks.compile.JavaCompile::class.java).configureEach {
167
+ targetCompatibility = "17"
168
+ }
169
+
170
+ project.logger.debug("Configured JDK 17 toolchain for ${project.name}")
171
+ }
172
+
173
+ /**
174
+ * Ensures autolinked modules are compiled with Java 17.
175
+ *
176
+ * Uses rootProject.allprojects to apply to already-configured projects.
177
+ * Registers callbacks independent of plugin application timing via pluginManager.withPlugin.
178
+ *
179
+ * Pattern from React Native Gradle Plugin's JdkConfiguratorUtils.kt
180
+ */
181
+ private fun configureNodeModulesJavaVersion(project: Project) {
182
+ // Apply to all projects using rootProject.allprojects (including already-configured projects)
183
+ project.rootProject.allprojects.forEach { targetProject ->
184
+ // Only target node_modules projects
185
+ if (!targetProject.projectDir.absolutePath.contains("/node_modules/")) return@forEach
186
+
187
+ // Configure Java 17 when Android Library plugin is applied
188
+ targetProject.pluginManager.withPlugin("com.android.library") {
189
+ val componentsExtension = targetProject.extensions
190
+ .getByType(LibraryAndroidComponentsExtension::class.java)
191
+ componentsExtension.finalizeDsl { libraryExtension ->
192
+ libraryExtension.compileOptions.apply {
193
+ sourceCompatibility = JavaVersion.VERSION_17
194
+ targetCompatibility = JavaVersion.VERSION_17
195
+ }
196
+ }
197
+ targetProject.logger.debug("Granite: Configured Java 17 for node_modules project: ${targetProject.name}")
198
+ }
199
+
200
+ // Configure jvmToolchain when Kotlin Android plugin is applied (using reflection)
201
+ targetProject.pluginManager.withPlugin("org.jetbrains.kotlin.android") {
202
+ try {
203
+ // Use reflection since KotlinAndroidProjectExtension has no compile-time dependency
204
+ val kotlinExtension = targetProject.extensions.findByName("kotlin")
205
+ if (kotlinExtension != null) {
206
+ val jvmToolchainMethod = kotlinExtension.javaClass.getMethod("jvmToolchain", Int::class.javaPrimitiveType)
207
+ jvmToolchainMethod.invoke(kotlinExtension, 17)
208
+ targetProject.logger.debug("Granite: Configured Kotlin jvmToolchain(17) for: ${targetProject.name}")
209
+ }
210
+ } catch (e: Exception) {
211
+ // Fallback: jvmToolchain may not exist in some Kotlin versions
212
+ targetProject.logger.debug("Granite: Could not set jvmToolchain for ${targetProject.name}: ${e.message}")
213
+ }
214
+ }
215
+ }
216
+
217
+ project.logger.debug("Granite: Registered Java 17 configuration for all node_modules projects")
218
+ }
219
+
220
+ private fun configurePlugin(
221
+ project: Project,
222
+ extension: GraniteExtension,
223
+ androidComponents: LibraryAndroidComponentsExtension,
224
+ ) {
225
+ // Get Android extension
226
+ val androidExtension = project.extensions.getByType(LibraryExtension::class.java)
227
+
228
+ // Validate extension configuration
229
+ extension.validate()
230
+
231
+ // Configure Maven repositories
232
+ val repositoryConfigurator = RepositoryConfigurator(project, extension)
233
+ repositoryConfigurator.configure()
234
+
235
+ // Configure React Native dependencies
236
+ val dependencyConfigurator = DependencyConfigurator(project, extension)
237
+ dependencyConfigurator.configure()
238
+
239
+ // Run react-native config and autolink native module dependencies
240
+ try {
241
+ val autolinkingConfig = runReactNativeConfig(project, extension)
242
+ autolinkLibrariesWithApp(project, autolinkingConfig)
243
+ } catch (e: Exception) {
244
+ project.logger.warn("Failed to autolink native module dependencies: ${e.message}")
245
+ project.logger.debug("Autolinking failure details:", e)
246
+ }
247
+
248
+ // NDK/CMake settings already configured in apply() method before afterEvaluate
249
+ // Resource packaging already configured in apply() method before afterEvaluate
250
+
251
+ // Configure BuildConfig fields
252
+ val buildConfigConfigurator = BuildConfigConfigurator(project, extension)
253
+ buildConfigConfigurator.configure(androidExtension)
254
+
255
+ // Generate dev server resources for debug builds
256
+ val devServerResourceConfigurator = DevServerResourceConfigurator(project, extension)
257
+ devServerResourceConfigurator.configure()
258
+
259
+ // Register autolinking task
260
+ val autolinkingTask = registerAutolinkingTask(project, extension, androidExtension)
261
+
262
+ // Wire autolinking into compilation
263
+ wireAutolinkingIntoCompilation(project, androidExtension, autolinkingTask)
264
+
265
+ // Register codegen tasks
266
+ val (codegenSchemaTask, codegenArtifactsTask) = registerCodegenTasks(project, extension, androidExtension)
267
+
268
+ // Wire codegen tasks into compilation
269
+ wireCodegenIntoCompilation(project, androidExtension, autolinkingTask, codegenSchemaTask, codegenArtifactsTask)
270
+
271
+ // Note: Generated sources are registered early in apply() method before afterEvaluate
272
+ // to avoid "too late to add actions" errors with finalizeDsl and onVariants
273
+
274
+ // Bundle and asset packaging tasks were registered early (before afterEvaluate)
275
+ // Now configure task dependencies
276
+ configureBundleTaskDependencies(project, codegenArtifactsTask)
277
+
278
+ project.logger.lifecycle("Granite plugin configured for library module: ${project.name}")
279
+ }
280
+
281
+ /**
282
+ * Registers the AutolinkingTask.
283
+ *
284
+ * @return TaskProvider for the autolinking task
285
+ */
286
+ private fun registerAutolinkingTask(
287
+ project: Project,
288
+ extension: GraniteExtension,
289
+ androidExtension: LibraryExtension,
290
+ ): TaskProvider<AutolinkingTask> {
291
+ val taskProvider = project.tasks.register("graniteAutolinking", AutolinkingTask::class.java)
292
+
293
+ taskProvider.configure {
294
+ reactNativeDir.set(extension.reactNativeDir.get())
295
+ nodeModulesDir.set(extension.nodeModulesDir.get())
296
+ projectDir.set(project.layout.projectDirectory)
297
+ outputDir.set(project.layout.buildDirectory.dir("generated/autolinking"))
298
+
299
+ // PackageList must be in com.facebook.react package to match React Native convention
300
+ // This is the expected package that React Native code imports
301
+ packageName.set("com.facebook.react")
302
+ packageListFile.set(
303
+ project.layout.buildDirectory.file("generated/autolinking/src/main/java/com/facebook/react/PackageList.java"),
304
+ )
305
+
306
+ // JNI autolinking output
307
+ jniOutputDir.set(project.layout.buildDirectory.dir("generated/autolinking/src/main/jni"))
308
+ autolinkingHeaderFile.set(
309
+ project.layout.buildDirectory.file("generated/autolinking/src/main/jni/autolinking.h"),
310
+ )
311
+ autolinkingCppFile.set(
312
+ project.layout.buildDirectory.file("generated/autolinking/src/main/jni/autolinking.cpp"),
313
+ )
314
+ }
315
+
316
+ return taskProvider
317
+ }
318
+
319
+ /**
320
+ * Wires the autolinking task into the compilation process.
321
+ *
322
+ * Ensures that PackageList.kt is generated before compilation starts.
323
+ * Uses standard preBuild task dependency instead of pattern matching.
324
+ *
325
+ * Note: Source registration is handled by registerGeneratedSourcesInDsl/InVariants methods.
326
+ */
327
+ private fun wireAutolinkingIntoCompilation(
328
+ project: Project,
329
+ androidExtension: LibraryExtension,
330
+ autolinkingTask: TaskProvider<AutolinkingTask>,
331
+ ) {
332
+ // Use standard preBuild dependency - more robust than pattern matching
333
+ project.tasks.named("preBuild").configure {
334
+ dependsOn(autolinkingTask)
335
+ }
336
+
337
+ // Make CMake tasks depend on autolinking (for JNI files)
338
+ project.tasks.configureEach {
339
+ if (name.startsWith("configureCMake")) {
340
+ dependsOn(autolinkingTask)
341
+ }
342
+ if (name.startsWith("buildCMake")) {
343
+ dependsOn(autolinkingTask)
344
+ }
345
+ }
346
+
347
+ project.logger.debug("Autolinking task wired into preBuild and CMake")
348
+ }
349
+
350
+ /**
351
+ * Registers the Codegen tasks (schema and artifacts).
352
+ *
353
+ * @return Pair of TaskProvider for (schema task, artifacts task)
354
+ */
355
+ private fun registerCodegenTasks(
356
+ project: Project,
357
+ extension: GraniteExtension,
358
+ androidExtension: LibraryExtension,
359
+ ): Pair<TaskProvider<CodegenSchemaTask>, TaskProvider<CodegenArtifactsTask>> {
360
+ // Register CodegenSchemaTask
361
+ val schemaTask = project.tasks.register("graniteCodegenSchema", CodegenSchemaTask::class.java)
362
+
363
+ schemaTask.configure {
364
+ // Determine JavaScript source directories
365
+ val jsSources = mutableListOf<File>()
366
+ val mainJsDir = project.file("src/main/js")
367
+ if (mainJsDir.exists()) {
368
+ jsSources.add(mainJsDir)
369
+ }
370
+
371
+ jsSourceDirs.set(jsSources)
372
+ reactNativeDir.set(extension.reactNativeDir.get())
373
+ nodeModulesDir.set(extension.nodeModulesDir.get())
374
+ outputDir.set(project.layout.buildDirectory.dir("generated/codegen/schema"))
375
+ schemaFile.set(
376
+ project.layout.buildDirectory.file("generated/codegen/schema/schema.json"),
377
+ )
378
+ }
379
+
380
+ // Register CodegenArtifactsTask
381
+ val artifactsTask = project.tasks.register("graniteCodegenArtifacts", CodegenArtifactsTask::class.java)
382
+
383
+ artifactsTask.configure {
384
+ schemaFile.set(schemaTask.flatMap { task -> task.schemaFile })
385
+ reactNativeDir.set(extension.reactNativeDir.get())
386
+ nodeModulesDir.set(extension.nodeModulesDir.get())
387
+
388
+ val namespace = androidExtension.namespace
389
+ ?: project.group.toString().ifEmpty { "com.example.granite" }
390
+ packageName.set(namespace)
391
+ libraryName.set(project.name)
392
+
393
+ javaOutputDir.set(project.layout.buildDirectory.dir("generated/codegen/java"))
394
+ jniOutputDir.set(project.layout.buildDirectory.dir("generated/codegen/jni"))
395
+
396
+ // Artifacts task depends on schema task
397
+ dependsOn(schemaTask)
398
+ }
399
+
400
+ return Pair(schemaTask, artifactsTask)
401
+ }
402
+
403
+ /**
404
+ * Wires the codegen tasks into the compilation process.
405
+ *
406
+ * Ensures that:
407
+ * - Codegen runs after autolinking
408
+ * - Codegen completes before compilation starts
409
+ *
410
+ * Uses standard preBuild task dependency instead of pattern matching.
411
+ *
412
+ * Note: Source registration is handled by registerGeneratedSourcesInDsl/InVariants methods.
413
+ * Note: C++ sources are handled by CMake configuration, not source sets.
414
+ */
415
+ private fun wireCodegenIntoCompilation(
416
+ project: Project,
417
+ androidExtension: LibraryExtension,
418
+ autolinkingTask: TaskProvider<AutolinkingTask>,
419
+ codegenSchemaTask: TaskProvider<CodegenSchemaTask>,
420
+ codegenArtifactsTask: TaskProvider<CodegenArtifactsTask>,
421
+ ) {
422
+ // Codegen schema should run after autolinking
423
+ codegenSchemaTask.configure {
424
+ mustRunAfter(autolinkingTask)
425
+ }
426
+
427
+ // Use standard preBuild dependency - more robust than pattern matching
428
+ project.tasks.named("preBuild").configure {
429
+ dependsOn(codegenArtifactsTask)
430
+ }
431
+
432
+ project.logger.debug("Codegen tasks wired into preBuild")
433
+ }
434
+
435
+ /**
436
+ * Registers bundle and asset packaging tasks for each build variant.
437
+ *
438
+ * Creates per-variant tasks for:
439
+ * - JavaScript bundling with Metro (graniteBundleXxx)
440
+ * - Hermes bytecode compilation
441
+ * - Asset packaging (granitePackageAssetsXxx)
442
+ *
443
+ * IMPORTANT: These tasks are NOT automatically executed during AAR builds.
444
+ * Users must manually run them when bundle generation is needed.
445
+ *
446
+ * Example usage:
447
+ * # Generate bundle only
448
+ * ./gradlew graniteBundleRelease
449
+ *
450
+ * # Generate bundle and package assets
451
+ * ./gradlew graniteBundleRelease granitePackageAssetsRelease
452
+ *
453
+ * # Build AAR with bundled assets
454
+ * ./gradlew graniteBundleRelease granitePackageAssetsRelease bundleReleaseAar
455
+ *
456
+ * Must be called BEFORE afterEvaluate to avoid "too late to add actions" error.
457
+ */
458
+ private fun registerBundleAndAssetTasksEarly(
459
+ project: Project,
460
+ extension: GraniteExtension,
461
+ componentsExtension: LibraryAndroidComponentsExtension,
462
+ ) {
463
+ componentsExtension.onVariants { variant ->
464
+ val variantName = variant.name
465
+ val capitalizedVariantName = variantName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
466
+
467
+ // Determine if this is a debug/dev variant
468
+ val isDev = variantName.contains("debug", ignoreCase = true)
469
+
470
+ // Register bundle task for this variant
471
+ val bundleTaskProvider = project.tasks.register(
472
+ "graniteBundle$capitalizedVariantName",
473
+ BundleTask::class.java,
474
+ )
475
+
476
+ bundleTaskProvider.configure {
477
+ entryFile.set(extension.getEntryFileResolved())
478
+ reactNativeDir.set(extension.reactNativeDir.get())
479
+ nodeModulesDir.set(extension.nodeModulesDir.get())
480
+ projectDir.set(project.layout.projectDirectory)
481
+
482
+ // Output bundle file (always Hermes bytecode .hbc)
483
+ val bundleName = extension.bundleAssetName.get()
484
+ bundleFile.set(
485
+ project.layout.buildDirectory.file(
486
+ "generated/assets/$variantName/$bundleName.hbc",
487
+ ),
488
+ )
489
+
490
+ // Source map file
491
+ sourceMapFile.set(
492
+ project.layout.buildDirectory.file(
493
+ "generated/assets/$variantName/$bundleName.map",
494
+ ),
495
+ )
496
+
497
+ bundleAssetName.set(extension.bundleAssetName.get())
498
+ this.devMode.set(isDev)
499
+ this.variantName.set(variantName)
500
+
501
+ // Bundle task should run after codegen (will be configured in afterEvaluate)
502
+ // mustRunAfter will be set up later when codegen task is registered
503
+ }
504
+
505
+ // Register asset packaging task for this variant
506
+ val assetPackagingTaskProvider = project.tasks.register(
507
+ "granitePackageAssets$capitalizedVariantName",
508
+ AssetPackagingTask::class.java,
509
+ )
510
+
511
+ assetPackagingTaskProvider.configure {
512
+ bundleFile.set(bundleTaskProvider.flatMap { it.bundleFile })
513
+
514
+ // Assets directory (Metro generates this during bundling)
515
+ assetsDir.set(
516
+ project.layout.buildDirectory.dir("generated/assets/$variantName"),
517
+ )
518
+
519
+ // Output to variant-specific Android assets directory
520
+ outputAssetsDir.set(
521
+ project.layout.projectDirectory.dir("src/$variantName/assets"),
522
+ )
523
+
524
+ outputResDir.set(
525
+ project.layout.projectDirectory.dir("src/$variantName/res"),
526
+ )
527
+
528
+ bundleAssetName.set(extension.bundleAssetName.get())
529
+ compressionEnabled.set(extension.bundleCompressionEnabled.get())
530
+ this.variantName.set(variantName)
531
+
532
+ // NOTE: No automatic dependency on bundleTaskProvider
533
+ // Users must manually run bundle tasks when needed:
534
+ // ./gradlew graniteBundle$capitalizedVariantName granitePackageAssets$capitalizedVariantName
535
+ }
536
+
537
+ // NOTE: Bundle and asset packaging tasks are decoupled from AAR build
538
+ // This allows:
539
+ // 1. Fast AAR builds without bundling (development)
540
+ // 2. Manual bundle generation when needed (production)
541
+ // 3. Flexibility for projects with JS in separate repositories
542
+ //
543
+ // To include bundles in AAR, manually run:
544
+ // ./gradlew graniteBundle$capitalizedVariantName granitePackageAssets$capitalizedVariantName bundle${capitalizedVariantName}Aar
545
+
546
+ project.logger.debug("Registered bundle and asset tasks for variant: $variantName")
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Configures bundle task dependencies after all tasks are registered.
552
+ * Called from afterEvaluate to set up dependency relationships.
553
+ */
554
+ private fun configureBundleTaskDependencies(
555
+ project: Project,
556
+ codegenArtifactsTask: TaskProvider<CodegenArtifactsTask>,
557
+ ) {
558
+ // Find all bundle tasks and configure them to run after codegen
559
+ project.tasks.withType(BundleTask::class.java).configureEach {
560
+ mustRunAfter(codegenArtifactsTask)
561
+ }
562
+
563
+ project.logger.debug("Configured bundle task dependencies")
564
+ }
565
+
566
+ /**
567
+ * Registers generated source directories at the DSL level using finalizeDsl.
568
+ *
569
+ * This ensures DSL-level source registration completes before variant processing,
570
+ * making generated sources visible in IDE and local builds.
571
+ *
572
+ * Pattern from React Native Gradle Plugin reference implementation.
573
+ */
574
+ private fun registerGeneratedSourcesInDsl(
575
+ project: Project,
576
+ androidComponents: LibraryAndroidComponentsExtension,
577
+ ) {
578
+ androidComponents.finalizeDsl { extension ->
579
+ // Register autolinking output
580
+ val autolinkingDir = project.layout.buildDirectory.dir("generated/autolinking/src/main/java")
581
+ extension.sourceSets.getByName("main").java.srcDir(autolinkingDir)
582
+
583
+ // Register codegen output
584
+ val codegenDir = project.layout.buildDirectory.dir("generated/codegen/java")
585
+ extension.sourceSets.getByName("main").java.srcDir(codegenDir)
586
+
587
+ project.logger.debug("Registered generated sources in DSL via finalizeDsl")
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Registers generated source directories at the variant level using onVariants.
593
+ *
594
+ * This is CRITICAL for dependent module visibility. Dependent modules read
595
+ * variant-level source configurations, not DSL-level.
596
+ *
597
+ * Uses addStaticSourceDirectory to register sources with each build variant,
598
+ * ensuring they are visible to:
599
+ * - Dependent modules during their configuration phase
600
+ * - AAR metadata for library publication
601
+ * - Android Studio IDE
602
+ *
603
+ * Pattern from React Native Gradle Plugin reference implementation.
604
+ */
605
+ private fun registerGeneratedSourcesInVariants(
606
+ project: Project,
607
+ androidComponents: LibraryAndroidComponentsExtension,
608
+ ) {
609
+ androidComponents.onVariants(androidComponents.selector().all()) { variant ->
610
+ // Register autolinking sources at variant level
611
+ val autolinkingDir = project.layout.buildDirectory
612
+ .dir("generated/autolinking/src/main/java")
613
+ .get()
614
+ .asFile
615
+ .absolutePath
616
+ variant.sources.java?.addStaticSourceDirectory(autolinkingDir)
617
+
618
+ // Register codegen sources at variant level
619
+ val codegenDir = project.layout.buildDirectory
620
+ .dir("generated/codegen/java")
621
+ .get()
622
+ .asFile
623
+ .absolutePath
624
+ variant.sources.java?.addStaticSourceDirectory(codegenDir)
625
+
626
+ project.logger.debug("Registered generated sources for variant ${variant.name} via onVariants")
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Runs react-native config command and parses the output.
632
+ *
633
+ * @param project The Gradle project
634
+ * @param extension The Granite extension with configuration
635
+ * @return Parsed autolinking configuration
636
+ */
637
+ private fun runReactNativeConfig(
638
+ project: Project,
639
+ extension: GraniteExtension,
640
+ ): run.granite.gradle.models.AutolinkingConfig {
641
+ val reactNativeDir: File = extension.reactNativeDir.get()
642
+ val cliPath = File(reactNativeDir, "cli.js")
643
+
644
+ if (!cliPath.exists()) {
645
+ error(
646
+ """
647
+ |React Native CLI not found: ${cliPath.absolutePath}
648
+ |
649
+ |Solution: Run 'npm install' or 'yarn install' to install React Native
650
+ """.trimMargin(),
651
+ )
652
+ }
653
+
654
+ // Find node executable
655
+ val nodeExecutable = NodeExecutableFinder.findNodeExecutable()
656
+
657
+ // Execute: node path/to/react-native/cli.js config
658
+ val command = listOf(
659
+ nodeExecutable.absolutePath,
660
+ cliPath.absolutePath,
661
+ "config",
662
+ )
663
+
664
+ val stdout = ByteArrayOutputStream()
665
+ val stderr = ByteArrayOutputStream()
666
+
667
+ val result = project.exec {
668
+ workingDir = project.projectDir
669
+ commandLine = command
670
+ standardOutput = stdout
671
+ errorOutput = stderr
672
+ isIgnoreExitValue = true
673
+ }
674
+
675
+ if (result.exitValue != 0) {
676
+ error(
677
+ """
678
+ |Failed to execute react-native config command.
679
+ |
680
+ |Command: ${command.joinToString(" ")}
681
+ |Exit code: ${result.exitValue}
682
+ |Error output:
683
+ |$stderr
684
+ """.trimMargin(),
685
+ )
686
+ }
687
+
688
+ val configJson = stdout.toString()
689
+
690
+ // Parse configuration
691
+ return try {
692
+ AutolinkingParser.parse(configJson)
693
+ } catch (e: IllegalArgumentException) {
694
+ throw IllegalArgumentException(
695
+ "react-native config: Failed to parse output. ${e.message}",
696
+ e,
697
+ )
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Extracts Gradle dependency configuration pairs from autolinking config.
703
+ *
704
+ * @param config Autolinking configuration from react-native config
705
+ * @return List of (configuration, projectPath) pairs
706
+ */
707
+ private fun getGradleDependenciesToApply(
708
+ config: run.granite.gradle.models.AutolinkingConfig,
709
+ ): List<Pair<String, String>> {
710
+ val result = mutableListOf<Pair<String, String>>()
711
+
712
+ config.androidDependencies()
713
+ .filter { !it.isPureCxxDependency }
714
+ .forEach { module ->
715
+ // Remove @ prefix and replace / with _ to match settings.gradle project names
716
+ // e.g., "@react-native-async-storage/async-storage" -> "react-native-async-storage_async-storage"
717
+ val nameCleansed = module.name.removePrefix("@").replace("/", "_")
718
+ val dependencyConfiguration = module.dependencyConfiguration ?: "api"
719
+ val buildTypes = module.buildTypes
720
+
721
+ if (buildTypes.isEmpty()) {
722
+ // No build types specified - use base configuration
723
+ result.add(dependencyConfiguration to ":$nameCleansed")
724
+ } else {
725
+ // Build type-specific dependencies
726
+ buildTypes.forEach { buildType ->
727
+ val config = "${buildType}${dependencyConfiguration.replaceFirstChar { it.uppercase() }}"
728
+ result.add(config to ":$nameCleansed")
729
+ }
730
+ }
731
+ }
732
+
733
+ return result
734
+ }
735
+
736
+ /**
737
+ * Programmatically adds Gradle project() dependencies for all autolinked native modules.
738
+ *
739
+ * Must be called in afterEvaluate block to ensure all subprojects are created.
740
+ *
741
+ * @param project The Gradle project where dependencies should be added
742
+ * @param config Autolinking configuration from react-native config
743
+ */
744
+ private fun autolinkLibrariesWithApp(
745
+ project: Project,
746
+ config: run.granite.gradle.models.AutolinkingConfig,
747
+ ) {
748
+ val dependencies = getGradleDependenciesToApply(config)
749
+
750
+ if (dependencies.isEmpty()) {
751
+ project.logger.lifecycle("No autolinked dependencies to add")
752
+ return
753
+ }
754
+
755
+ project.logger.lifecycle("Autolinking ${dependencies.size} native module dependencies...")
756
+
757
+ var successCount = 0
758
+ var failureCount = 0
759
+
760
+ dependencies.forEach { (configuration, projectPath) ->
761
+ // Validate project exists before adding dependency
762
+ val foundProject = project.rootProject.findProject(projectPath)
763
+ if (foundProject != null) {
764
+ try {
765
+ project.dependencies.add(
766
+ configuration,
767
+ project.dependencies.project(mapOf("path" to projectPath)),
768
+ )
769
+ project.logger.lifecycle("✓ Added dependency: $configuration '$projectPath'")
770
+ successCount++
771
+ } catch (e: Exception) {
772
+ project.logger.error("✗ Failed to add dependency $configuration '$projectPath': ${e.message}")
773
+ failureCount++
774
+ }
775
+ } else {
776
+ project.logger.error("✗ Skipping autolink for $projectPath - project not found in settings.gradle")
777
+ project.logger.debug(" Available projects: ${project.rootProject.subprojects.map { it.path }}")
778
+ failureCount++
779
+ }
780
+ }
781
+
782
+ project.logger.lifecycle("Autolinking complete. Added $successCount dependencies successfully${if (failureCount > 0) ", $failureCount failed" else ""}.")
783
+ }
784
+ }