@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,431 @@
1
+ package run.granite.gradle.tasks
2
+
3
+ import com.google.gson.Gson
4
+ import com.google.gson.JsonObject
5
+ import org.gradle.api.DefaultTask
6
+ import org.gradle.api.file.DirectoryProperty
7
+ import org.gradle.api.file.RegularFileProperty
8
+ import org.gradle.api.provider.Property
9
+ import org.gradle.api.tasks.*
10
+ import org.gradle.process.ExecOperations
11
+ import run.granite.gradle.generators.CMakeGenerator
12
+ import run.granite.gradle.generators.CppAutolinkingGenerator
13
+ import run.granite.gradle.generators.EntryPointGenerator
14
+ import run.granite.gradle.models.AutolinkingConfig
15
+ import run.granite.gradle.models.NativeModule
16
+ import run.granite.gradle.utils.AutolinkingParser
17
+ import run.granite.gradle.utils.NodeExecutableFinder
18
+ import java.io.ByteArrayOutputStream
19
+ import java.io.File
20
+ import javax.inject.Inject
21
+
22
+ /**
23
+ * Gradle task for React Native autolinking.
24
+ *
25
+ * This task:
26
+ * 1. Executes `react-native config` command to discover native modules
27
+ * 2. Parses the autolinking configuration JSON
28
+ * 3. Generates PackageList.kt file with all discovered React Native packages
29
+ *
30
+ * The generated PackageList.kt is used by the React Native initialization code
31
+ * to automatically register all native modules without manual configuration.
32
+ *
33
+ * Inputs:
34
+ * - React Native CLI path
35
+ * - Node modules directory
36
+ * - Project root directory
37
+ *
38
+ * Outputs:
39
+ * - PackageList.kt file in build/generated/autolinking/
40
+ */
41
+ @CacheableTask
42
+ abstract class AutolinkingTask @Inject constructor(
43
+ private val execOperations: ExecOperations,
44
+ ) : DefaultTask() {
45
+
46
+ /**
47
+ * React Native directory containing the CLI.
48
+ * Marked as Internal to avoid task output overlaps with other modules.
49
+ */
50
+ @get:Internal
51
+ abstract val reactNativeDir: DirectoryProperty
52
+
53
+ /**
54
+ * Node modules directory for dependency resolution.
55
+ * Marked as Internal to avoid task output overlaps with other modules.
56
+ */
57
+ @get:Internal
58
+ abstract val nodeModulesDir: DirectoryProperty
59
+
60
+ /**
61
+ * Project root directory.
62
+ * Marked as Internal to avoid task output overlaps with other modules.
63
+ */
64
+ @get:Internal
65
+ abstract val projectDir: DirectoryProperty
66
+
67
+ /**
68
+ * Output directory for generated PackageList.kt.
69
+ */
70
+ @get:OutputDirectory
71
+ abstract val outputDir: DirectoryProperty
72
+
73
+ /**
74
+ * Generated PackageList.kt file.
75
+ */
76
+ @get:OutputFile
77
+ abstract val packageListFile: RegularFileProperty
78
+
79
+ /**
80
+ * Package name for the generated PackageList class.
81
+ */
82
+ @get:Input
83
+ abstract val packageName: Property<String>
84
+
85
+ /**
86
+ * Output directory for generated JNI autolinking files.
87
+ */
88
+ @get:OutputDirectory
89
+ abstract val jniOutputDir: DirectoryProperty
90
+
91
+ /**
92
+ * Generated autolinking.h header file.
93
+ */
94
+ @get:OutputFile
95
+ abstract val autolinkingHeaderFile: RegularFileProperty
96
+
97
+ /**
98
+ * Generated autolinking.cpp implementation file.
99
+ */
100
+ @get:OutputFile
101
+ abstract val autolinkingCppFile: RegularFileProperty
102
+
103
+ init {
104
+ group = "granite"
105
+ description = "Executes React Native autolinking and generates PackageList.kt"
106
+ }
107
+
108
+ @TaskAction
109
+ fun execute() {
110
+ logger.lifecycle("Running React Native autolinking...")
111
+
112
+ // Execute react-native config once and reuse results
113
+ val configJson = executeReactNativeConfig()
114
+
115
+ // Parse configuration using AutolinkingParser
116
+ val autolinkingConfig = try {
117
+ AutolinkingParser.parse(configJson)
118
+ } catch (e: IllegalArgumentException) {
119
+ // Fail-fast with descriptive error message
120
+ throw IllegalArgumentException(
121
+ "react-native config: Failed to parse output. ${e.message}",
122
+ e,
123
+ )
124
+ }
125
+
126
+ val modules = autolinkingConfig.androidDependencies()
127
+ val javaModules = autolinkingConfig.javaModules()
128
+
129
+ // Generate all autolinking files in a single task execution
130
+ generatePackageList(javaModules)
131
+ generateJniAutolinking(modules, autolinkingConfig)
132
+
133
+ logger.lifecycle("Autolinking complete. Found ${modules.size} native modules (${javaModules.size} Java, ${autolinkingConfig.cxxModules().size} C++, ${autolinkingConfig.fabricModules().size} Fabric).")
134
+ }
135
+
136
+ /**
137
+ * Executes `react-native config` command and returns the JSON output.
138
+ *
139
+ * Implementation details in corresponding task.
140
+ */
141
+ private fun executeReactNativeConfig(): String {
142
+ val cliPath = reactNativeDir.get().file("cli.js").asFile
143
+ val nodeExecutable = NodeExecutableFinder.findNodeExecutable()
144
+
145
+ if (!cliPath.exists()) {
146
+ error(
147
+ """
148
+ |React Native CLI not found: ${cliPath.absolutePath}
149
+ |
150
+ |Solution: Run 'npm install' or 'yarn install' to install React Native
151
+ """.trimMargin(),
152
+ )
153
+ }
154
+
155
+ // Execute: node path/to/react-native/cli.js config
156
+ val command = listOf(
157
+ nodeExecutable.absolutePath,
158
+ cliPath.absolutePath,
159
+ "config",
160
+ )
161
+
162
+ val stdout = ByteArrayOutputStream()
163
+ val stderr = ByteArrayOutputStream()
164
+
165
+ val result = execOperations.exec {
166
+ workingDir = projectDir.get().asFile
167
+ commandLine = command
168
+ standardOutput = stdout
169
+ errorOutput = stderr
170
+ isIgnoreExitValue = true
171
+ }
172
+
173
+ if (result.exitValue != 0) {
174
+ error(
175
+ """
176
+ |Failed to execute react-native config command.
177
+ |
178
+ |Command: ${command.joinToString(" ")}
179
+ |Exit code: ${result.exitValue}
180
+ |Error output:
181
+ |$stderr
182
+ """.trimMargin(),
183
+ )
184
+ }
185
+
186
+ return stdout.toString()
187
+ }
188
+
189
+ /**
190
+ * Interpolates dynamic values (BuildConfig, R) in package instances.
191
+ * Replaces non-FQDN references with fully qualified package names.
192
+ */
193
+ private fun interpolateDynamicValues(input: String, pkgName: String): String = input.replace(Regex("([^.\\w])(BuildConfig|R)(\\W)")) { match ->
194
+ val (prefix, className, suffix) = match.destructured
195
+ "${prefix}$pkgName.${className}$suffix"
196
+ }
197
+
198
+ /**
199
+ * Composes package import statements from native modules.
200
+ * Filters out pure C++ dependencies.
201
+ */
202
+ private fun composePackageImports(modules: List<run.granite.gradle.models.NativeModule>): String = modules
203
+ .filter { !it.isPureCxxDependency }
204
+ .mapNotNull { module ->
205
+ module.packageImportPath?.let { importPath ->
206
+ val cleanImportPath = importPath
207
+ .removePrefix("import ")
208
+ .removeSuffix(";")
209
+ .trim()
210
+ interpolateDynamicValues("import $cleanImportPath;", packageName.get())
211
+ }
212
+ }
213
+ .joinToString("\n")
214
+
215
+ /**
216
+ * Composes package instance code for getPackages() method.
217
+ * Filters out pure C++ dependencies.
218
+ */
219
+ private fun composePackageInstance(modules: List<run.granite.gradle.models.NativeModule>): String = modules
220
+ .filter { !it.isPureCxxDependency }
221
+ .mapIndexed { index, module ->
222
+ val instanceCode = interpolateDynamicValues(
223
+ module.packageInstance ?: "new UnknownPackage()",
224
+ packageName.get(),
225
+ )
226
+ if (index == 0) ",\n $instanceCode" else ",\n $instanceCode"
227
+ }
228
+ .joinToString("")
229
+
230
+ /**
231
+ * Generates PackageList.java file from discovered Java packages.
232
+ */
233
+ private fun generatePackageList(modules: List<run.granite.gradle.models.NativeModule>) {
234
+ val outputFile = packageListFile.get().asFile
235
+ outputFile.parentFile.mkdirs()
236
+
237
+ val packageImports = composePackageImports(modules)
238
+ val packageInstances = composePackageInstance(modules)
239
+
240
+ val packageListContent = """
241
+ // Generated by Granite Gradle Plugin - DO NOT EDIT
242
+ // This file is automatically generated during the build process
243
+ package com.facebook.react;
244
+
245
+ import android.app.Application;
246
+ import android.content.Context;
247
+ import android.content.res.Resources;
248
+
249
+ import com.facebook.react.ReactPackage;
250
+ import com.facebook.react.ReactNativeHost;
251
+ import com.facebook.react.shell.MainPackageConfig;
252
+ import com.facebook.react.shell.MainReactPackage;
253
+ import java.util.Arrays;
254
+ import java.util.ArrayList;
255
+
256
+ $packageImports
257
+
258
+ @SuppressWarnings("deprecation")
259
+ public class PackageList {
260
+ private Application application;
261
+ private ReactNativeHost reactNativeHost;
262
+ private MainPackageConfig mConfig;
263
+
264
+ public PackageList(ReactNativeHost reactNativeHost) {
265
+ this(reactNativeHost, null);
266
+ }
267
+
268
+ public PackageList(Application application) {
269
+ this(application, null);
270
+ }
271
+
272
+ public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
273
+ this.reactNativeHost = reactNativeHost;
274
+ mConfig = config;
275
+ }
276
+
277
+ public PackageList(Application application, MainPackageConfig config) {
278
+ this.reactNativeHost = null;
279
+ this.application = application;
280
+ mConfig = config;
281
+ }
282
+
283
+ private ReactNativeHost getReactNativeHost() {
284
+ return this.reactNativeHost;
285
+ }
286
+
287
+ private Resources getResources() {
288
+ return this.getApplication().getResources();
289
+ }
290
+
291
+ private Application getApplication() {
292
+ if (this.reactNativeHost == null) return this.application;
293
+ return this.reactNativeHost.getApplication();
294
+ }
295
+
296
+ private Context getApplicationContext() {
297
+ return this.getApplication().getApplicationContext();
298
+ }
299
+
300
+ public ArrayList<ReactPackage> getPackages() {
301
+ return new ArrayList<>(Arrays.<ReactPackage>asList(
302
+ new MainReactPackage(mConfig)$packageInstances
303
+ ));
304
+ }
305
+ }
306
+ """.trimIndent()
307
+
308
+ outputFile.writeText(packageListContent)
309
+ logger.lifecycle("Generated PackageList.java: ${outputFile.absolutePath}")
310
+ }
311
+
312
+ /**
313
+ * Generates JNI autolinking files (autolinking.h, autolinking.cpp, Android-autolinking.cmake)
314
+ * and Entry Point (ReactNativeApplicationEntryPoint.java).
315
+ */
316
+ private fun generateJniAutolinking(
317
+ modules: List<NativeModule>,
318
+ config: AutolinkingConfig,
319
+ ) {
320
+ val jniDir = jniOutputDir.get().asFile
321
+ val javaOutputDir = outputDir.get().asFile
322
+ val projectRoot = projectDir.get().asFile
323
+
324
+ // Generate autolinking.h (header with forward declarations)
325
+ generateAutolinkingHeader(modules)
326
+
327
+ // Generate autolinking.cpp using CppAutolinkingGenerator
328
+ CppAutolinkingGenerator.generateToFile(modules, jniDir)
329
+ logger.lifecycle("Generated autolinking.cpp with ${modules.filter { it.needsCppAutolinking }.size} modules")
330
+
331
+ // Generate Android-autolinking.cmake using CMakeGenerator
332
+ val cmakeModules = modules.filter { it.hasCMakeConfiguration }
333
+ if (cmakeModules.isNotEmpty()) {
334
+ CMakeGenerator.generateToFile(modules, projectRoot, jniDir)
335
+ logger.lifecycle("Generated Android-autolinking.cmake with ${cmakeModules.size} CMake modules")
336
+ } else {
337
+ logger.lifecycle("Skipped Android-autolinking.cmake generation - no CMake modules found")
338
+ }
339
+
340
+ // Generate ReactNativeApplicationEntryPoint.java using EntryPointGenerator
341
+ val packageName = config.project.android?.packageName
342
+ try {
343
+ EntryPointGenerator.generateToFile(packageName, javaOutputDir)
344
+ logger.lifecycle("Generated ReactNativeApplicationEntryPoint.java")
345
+ } catch (e: IllegalStateException) {
346
+ // Package name validation error
347
+ logger.warn("Skipped entry point generation: ${e.message}")
348
+ }
349
+
350
+ logger.lifecycle("Generated all autolinking files in: ${jniDir.absolutePath}")
351
+ }
352
+
353
+ /**
354
+ * Generates autolinking.h header file with forward declarations.
355
+ */
356
+ private fun generateAutolinkingHeader(modules: List<run.granite.gradle.models.NativeModule>) {
357
+ val headerFile = autolinkingHeaderFile.get().asFile
358
+ headerFile.parentFile.mkdirs()
359
+
360
+ val cxxModules = modules.filter { it.hasCxxImplementation }
361
+ val javaModules = modules.filter { it.hasJavaImplementation }
362
+
363
+ val headerContent = buildString {
364
+ appendLine("// Generated by Granite Gradle Plugin - DO NOT EDIT")
365
+ appendLine("// This file is automatically generated during the build process")
366
+ appendLine()
367
+ appendLine("#pragma once")
368
+ appendLine()
369
+ appendLine("#include <memory>")
370
+ appendLine("#include <string>")
371
+ appendLine()
372
+ appendLine("#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>")
373
+ appendLine("#include <react/bridging/Bridging.h>")
374
+ appendLine("#include <ReactCommon/CallInvokerHolder.h>")
375
+ appendLine("#include <ReactCommon/TurboModule.h>")
376
+ appendLine("#include <ReactCommon/JavaTurboModule.h>")
377
+ appendLine()
378
+ appendLine("namespace facebook::react {")
379
+ appendLine()
380
+
381
+ // Forward declarations for C++ TurboModule providers
382
+ if (cxxModules.isNotEmpty()) {
383
+ appendLine("// Forward declarations for C++ TurboModule providers")
384
+ cxxModules.forEach { module ->
385
+ module.cxxModuleHeaderName?.let { headerName ->
386
+ appendLine("class $headerName;")
387
+ }
388
+ }
389
+ appendLine()
390
+ }
391
+
392
+ // Forward declarations for Java TurboModule providers
393
+ if (javaModules.isNotEmpty()) {
394
+ appendLine("// Forward declarations for Java TurboModule providers")
395
+ javaModules.forEach { module ->
396
+ module.libraryName?.let { libraryName ->
397
+ appendLine("std::shared_ptr<TurboModule> ${libraryName}_ModuleProvider(")
398
+ appendLine(" const std::string& moduleName,")
399
+ appendLine(" const JavaTurboModule::InitParams& params);")
400
+ appendLine()
401
+ }
402
+ }
403
+ }
404
+
405
+ appendLine("/**")
406
+ appendLine(" * Registers Fabric component providers from autolinked libraries.")
407
+ appendLine(" */")
408
+ appendLine("void autolinking_registerProviders(")
409
+ appendLine(" std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);")
410
+ appendLine()
411
+ appendLine("/**")
412
+ appendLine(" * Provides C++ TurboModule from autolinked libraries.")
413
+ appendLine(" */")
414
+ appendLine("std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(")
415
+ appendLine(" const std::string& name,")
416
+ appendLine(" const std::shared_ptr<CallInvoker>& jsInvoker);")
417
+ appendLine()
418
+ appendLine("/**")
419
+ appendLine(" * Provides Java TurboModule from autolinked libraries.")
420
+ appendLine(" */")
421
+ appendLine("std::shared_ptr<TurboModule> autolinking_ModuleProvider(")
422
+ appendLine(" const std::string& name,")
423
+ appendLine(" const JavaTurboModule::InitParams& params);")
424
+ appendLine()
425
+ appendLine("} // namespace facebook::react")
426
+ }
427
+
428
+ headerFile.writeText(headerContent)
429
+ logger.debug("Generated autolinking.h: ${headerFile.absolutePath}")
430
+ }
431
+ }
@@ -0,0 +1,275 @@
1
+ package run.granite.gradle.tasks
2
+
3
+ import org.gradle.api.DefaultTask
4
+ import org.gradle.api.file.DirectoryProperty
5
+ import org.gradle.api.file.RegularFileProperty
6
+ import org.gradle.api.provider.Property
7
+ import org.gradle.api.tasks.*
8
+ import org.gradle.process.ExecOperations
9
+ import run.granite.gradle.utils.NodeExecutableFinder
10
+ import java.io.ByteArrayOutputStream
11
+ import java.io.File
12
+ import javax.inject.Inject
13
+
14
+ /**
15
+ * Gradle task for generating React Native JavaScript bundles.
16
+ *
17
+ * This task:
18
+ * 1. Executes Metro bundler to bundle JavaScript code
19
+ * 2. Compiles the bundle to Hermes bytecode (always enabled)
20
+ * 3. Handles dev vs release build configurations
21
+ * 4. Generates source maps for debugging
22
+ */
23
+ @CacheableTask
24
+ abstract class BundleTask @Inject constructor(
25
+ private val execOperations: ExecOperations,
26
+ ) : DefaultTask() {
27
+
28
+ @get:InputFile
29
+ @get:PathSensitive(PathSensitivity.RELATIVE)
30
+ abstract val entryFile: RegularFileProperty
31
+
32
+ /**
33
+ * React Native directory.
34
+ * Marked as Internal to avoid task output overlaps with other modules.
35
+ */
36
+ @get:Internal
37
+ abstract val reactNativeDir: DirectoryProperty
38
+
39
+ /**
40
+ * Node modules directory.
41
+ * Marked as Internal to avoid task output overlaps with other modules.
42
+ */
43
+ @get:Internal
44
+ abstract val nodeModulesDir: DirectoryProperty
45
+
46
+ /**
47
+ * Project root directory.
48
+ * Marked as Internal to avoid task output overlaps with other modules.
49
+ */
50
+ @get:Internal
51
+ abstract val projectDir: DirectoryProperty
52
+
53
+ @get:OutputFile
54
+ abstract val bundleFile: RegularFileProperty
55
+
56
+ @get:OutputFile
57
+ abstract val sourceMapFile: RegularFileProperty
58
+
59
+ @get:Input
60
+ abstract val bundleAssetName: Property<String>
61
+
62
+ @get:Input
63
+ abstract val devMode: Property<Boolean>
64
+
65
+ @get:Input
66
+ abstract val variantName: Property<String>
67
+
68
+ init {
69
+ group = "granite"
70
+ description = "Generates React Native JavaScript bundle"
71
+ }
72
+
73
+ @TaskAction
74
+ fun execute() {
75
+ val variant = variantName.get()
76
+ logger.lifecycle("Bundling JavaScript for variant: $variant")
77
+
78
+ // Validate entry file exists at execution time
79
+ // This allows flexibility for projects with JS in separate repos during development
80
+ val entryFileResolved = entryFile.get().asFile
81
+ if (!entryFileResolved.exists()) {
82
+ error(
83
+ """
84
+ |JavaScript entry file not found: ${entryFileResolved.absolutePath}
85
+ |
86
+ |The entry file is required for bundle generation but was not found.
87
+ |This typically occurs when:
88
+ | 1. JavaScript source files are in a separate repository
89
+ | 2. The entry file path is incorrectly configured
90
+ | 3. Build is running before JS files are available
91
+ |
92
+ |Solutions:
93
+ | 1. Ensure JavaScript files are available before running release builds
94
+ | 2. Configure the correct entry file path in build.gradle.kts:
95
+ | granite {
96
+ | entryFile.set("path/to/index.js")
97
+ | }
98
+ | 3. For development, use Metro dev server instead of bundling
99
+ |
100
+ |Variant: $variant
101
+ """.trimMargin(),
102
+ )
103
+ }
104
+
105
+ val jsBundleFile = generateJavaScriptBundle()
106
+
107
+ // Always compile to Hermes bytecode (JSC not supported)
108
+ compileToHermes(jsBundleFile)
109
+
110
+ logger.lifecycle("Bundle generation complete for variant: $variant")
111
+ }
112
+
113
+ private fun generateJavaScriptBundle(): File {
114
+ logger.lifecycle("Running Metro bundler...")
115
+
116
+ val cliPath = reactNativeDir.get().file("cli.js").asFile
117
+ val nodeExecutable = NodeExecutableFinder.findNodeExecutable()
118
+
119
+ if (!cliPath.exists()) {
120
+ error("React Native CLI not found: ${cliPath.absolutePath}")
121
+ }
122
+
123
+ val tempJsBundle = File(bundleFile.get().asFile.parentFile, "temp_${bundleAssetName.get()}")
124
+ tempJsBundle.parentFile.mkdirs()
125
+
126
+ val command = buildList {
127
+ add(nodeExecutable.absolutePath)
128
+ add(cliPath.absolutePath)
129
+ add("bundle")
130
+ add("--platform")
131
+ add("android")
132
+ add("--entry-file")
133
+ add(entryFile.get().asFile.absolutePath)
134
+ add("--bundle-output")
135
+ add(tempJsBundle.absolutePath)
136
+ add("--sourcemap-output")
137
+ add(sourceMapFile.get().asFile.absolutePath)
138
+ add("--assets-dest")
139
+ add(bundleFile.get().asFile.parentFile.absolutePath)
140
+
141
+ if (devMode.get()) {
142
+ add("--dev")
143
+ add("true")
144
+ } else {
145
+ add("--dev")
146
+ add("false")
147
+ add("--minify")
148
+ add("true")
149
+ }
150
+ }
151
+
152
+ val stdout = ByteArrayOutputStream()
153
+ val stderr = ByteArrayOutputStream()
154
+
155
+ val result = execOperations.exec {
156
+ workingDir = projectDir.get().asFile
157
+ commandLine = command
158
+ standardOutput = stdout
159
+ errorOutput = stderr
160
+ isIgnoreExitValue = true
161
+ }
162
+
163
+ if (result.exitValue != 0) {
164
+ error("Failed to bundle JavaScript. Exit code: ${result.exitValue}\n$stderr")
165
+ }
166
+
167
+ logger.lifecycle("Metro bundling complete: ${tempJsBundle.absolutePath}")
168
+ return tempJsBundle
169
+ }
170
+
171
+ private fun compileToHermes(jsBundleFile: File) {
172
+ logger.lifecycle("Compiling to Hermes bytecode...")
173
+
174
+ val hermesExecutable = findHermesCompiler()
175
+
176
+ val command = listOf(
177
+ hermesExecutable.absolutePath,
178
+ "-emit-binary",
179
+ "-out",
180
+ bundleFile.get().asFile.absolutePath,
181
+ jsBundleFile.absolutePath,
182
+ )
183
+
184
+ val stdout = ByteArrayOutputStream()
185
+ val stderr = ByteArrayOutputStream()
186
+
187
+ val result = execOperations.exec {
188
+ commandLine = command
189
+ standardOutput = stdout
190
+ errorOutput = stderr
191
+ isIgnoreExitValue = true
192
+ }
193
+
194
+ if (result.exitValue != 0) {
195
+ error("Failed to compile to Hermes bytecode. Exit code: ${result.exitValue}\n$stderr")
196
+ }
197
+
198
+ jsBundleFile.delete()
199
+
200
+ logger.lifecycle("Hermes compilation complete: ${bundleFile.get().asFile.absolutePath}")
201
+ }
202
+
203
+ private fun findHermesCompiler(): File {
204
+ // Determine OS-specific binary directory
205
+ val osBinDir = when {
206
+ System.getProperty("os.name").startsWith("Windows") -> "win64-bin"
207
+ System.getProperty("os.name").startsWith("Mac") -> "osx-bin"
208
+ else -> "linux64-bin"
209
+ }
210
+ val hermescName = if (osBinDir == "win64-bin") "hermesc.exe" else "hermesc"
211
+
212
+ val nodeModules = nodeModulesDir.get().asFile
213
+
214
+ // 1. Check known paths in hermes-engine package
215
+ val hermesEnginePaths = listOf(
216
+ File(nodeModules, "hermes-engine/$osBinDir/$hermescName"),
217
+ File(nodeModules, "hermes-engine/bin/$hermescName"),
218
+ )
219
+
220
+ for (path in hermesEnginePaths) {
221
+ if (path.exists() && path.canExecute()) {
222
+ return path
223
+ }
224
+ }
225
+
226
+ // 2. Check known paths in react-native SDKs
227
+ val reactNativeSdkPaths = listOf(
228
+ File(nodeModules, "react-native/sdks/hermesc/$osBinDir/$hermescName"),
229
+ File(nodeModules, "react-native/sdks/hermes/$osBinDir/$hermescName"),
230
+ )
231
+
232
+ for (path in reactNativeSdkPaths) {
233
+ if (path.exists() && path.canExecute()) {
234
+ return path
235
+ }
236
+ }
237
+
238
+ // 3. Check react-native/android Maven cache (original location)
239
+ val reactNativeAndroid = File(reactNativeDir.get().asFile, "android")
240
+ val hermesEngine = File(reactNativeAndroid, "com/facebook/react/hermes-engine")
241
+
242
+ if (hermesEngine.exists()) {
243
+ val hermescExecutables = hermesEngine.walk()
244
+ .filter { it.name == hermescName }
245
+ .filter { it.canExecute() }
246
+ .toList()
247
+
248
+ if (hermescExecutables.isNotEmpty()) {
249
+ return hermescExecutables.first()
250
+ }
251
+ }
252
+
253
+ // 4. Fallback: targeted search in specific subdirectories only
254
+ val targetedDirs = listOf(
255
+ File(nodeModules, "hermes-engine"),
256
+ File(nodeModules, "react-native/sdks"),
257
+ )
258
+
259
+ for (dir in targetedDirs) {
260
+ if (dir.exists()) {
261
+ val found = dir.walk()
262
+ .maxDepth(5) // Limit depth to prevent excessive traversal
263
+ .filter { it.name == hermescName }
264
+ .filter { it.canExecute() }
265
+ .firstOrNull()
266
+
267
+ if (found != null) {
268
+ return found
269
+ }
270
+ }
271
+ }
272
+
273
+ error("Hermes compiler (hermesc) not found. Searched paths: ${hermesEnginePaths + reactNativeSdkPaths}")
274
+ }
275
+ }