@nebula-rn/host 0.0.1

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 (37) hide show
  1. package/NebulaHost.podspec +23 -0
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build.gradle +27 -0
  11. package/android/consumer-rules.pro +1 -0
  12. package/android/src/main/AndroidManifest.xml +1 -0
  13. package/android/src/main/java/com/hectorzhuang/nebula/NebulaActivity.kt +290 -0
  14. package/android/src/main/java/com/hectorzhuang/nebula/NebulaAppManager.kt +134 -0
  15. package/android/src/main/java/com/hectorzhuang/nebula/NebulaConfig.kt +324 -0
  16. package/android/src/main/java/com/hectorzhuang/nebula/NebulaEventHub.kt +49 -0
  17. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHost.kt +145 -0
  18. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHostModalActivity.kt +178 -0
  19. package/android/src/main/java/com/hectorzhuang/nebula/NebulaManifestManager.kt +130 -0
  20. package/android/src/main/java/com/hectorzhuang/nebula/NebulaNativeModule.kt +604 -0
  21. package/android/src/main/java/com/hectorzhuang/nebula/NebulaPackage.kt +16 -0
  22. package/android/src/main/java/com/hectorzhuang/nebula/NebulaRouter.kt +300 -0
  23. package/ios/Nebula/NebulaAppManager.swift +355 -0
  24. package/ios/Nebula/NebulaConfig.swift +549 -0
  25. package/ios/Nebula/NebulaContainerController.swift +580 -0
  26. package/ios/Nebula/NebulaDevLoading.swift +333 -0
  27. package/ios/Nebula/NebulaHost.swift +611 -0
  28. package/ios/Nebula/NebulaManifest.swift +214 -0
  29. package/ios/Nebula/NebulaNativeModule.swift +682 -0
  30. package/ios/Nebula/NebulaNativeModuleBridge.m +364 -0
  31. package/ios/Nebula/NebulaPerformanceMonitor.swift +46 -0
  32. package/ios/Nebula/NebulaRouter.swift +594 -0
  33. package/ios/Nebula/NebulaRouterBridge.m +19 -0
  34. package/ios/Nebula/RNInstanceViewController.swift +52 -0
  35. package/package.json +41 -0
  36. package/react-native.config.js +14 -0
  37. package/src/index.ts +9 -0
@@ -0,0 +1,23 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NebulaHost"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package.dig("repository", "url")
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+ s.platforms = { :ios => "15.1" }
13
+ s.source = { :git => package.dig("repository", "url"), :tag => s.version.to_s }
14
+
15
+ s.source_files = "ios/Nebula/**/*.{swift,m,h}"
16
+ s.requires_arc = true
17
+ s.swift_version = "5.0"
18
+
19
+ s.dependency "React-Core"
20
+ s.dependency "React-RCTAppDelegate"
21
+ s.dependency "ReactAppDependencyProvider"
22
+ s.dependency "ZIPFoundation"
23
+ end
File without changes
@@ -0,0 +1,2 @@
1
+ #Sat Jun 27 22:59:30 CST 2026
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,27 @@
1
+ apply plugin: "com.android.library"
2
+ apply plugin: "org.jetbrains.kotlin.android"
3
+
4
+ android {
5
+ namespace "com.hectorzhuang.nebula.host"
6
+ compileSdkVersion rootProject.ext.compileSdkVersion
7
+
8
+ defaultConfig {
9
+ minSdkVersion rootProject.ext.minSdkVersion
10
+ targetSdkVersion rootProject.ext.targetSdkVersion
11
+ consumerProguardFiles "consumer-rules.pro"
12
+ }
13
+
14
+ compileOptions {
15
+ sourceCompatibility JavaVersion.VERSION_17
16
+ targetCompatibility JavaVersion.VERSION_17
17
+ }
18
+
19
+ kotlinOptions {
20
+ jvmTarget = "17"
21
+ }
22
+ }
23
+
24
+ dependencies {
25
+ implementation("com.facebook.react:react-android")
26
+ implementation("androidx.appcompat:appcompat:1.7.1")
27
+ }
@@ -0,0 +1 @@
1
+ # Consumer rules placeholder for @nebula-rn/host.
@@ -0,0 +1 @@
1
+ <manifest package="com.hectorzhuang.nebula.host" />
@@ -0,0 +1,290 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.graphics.Color
6
+ import android.os.Bundle
7
+ import android.view.View
8
+ import android.view.ViewTreeObserver
9
+ import android.widget.FrameLayout
10
+ import android.widget.LinearLayout
11
+ import androidx.appcompat.app.AppCompatActivity
12
+ import androidx.appcompat.widget.Toolbar
13
+ import androidx.core.view.ViewCompat
14
+ import androidx.core.view.WindowInsetsCompat
15
+ import androidx.core.view.updatePadding
16
+ import com.facebook.react.interfaces.fabric.ReactSurface
17
+ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
18
+ import java.util.UUID
19
+
20
+ class NebulaActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
21
+ companion object {
22
+ private const val EXTRA_APP_ID = "nebula.app_id"
23
+ private const val EXTRA_ROUTE_PATH = "nebula.route_path"
24
+ private const val EXTRA_ROUTE_URL = "nebula.route_url"
25
+ private const val EXTRA_INITIAL_PROPS = "nebula.initial_props"
26
+ private const val EXTRA_INSTANCE_ID = "nebula.instance_id"
27
+
28
+ fun createIntent(
29
+ activity: Activity,
30
+ appId: String,
31
+ routePath: String,
32
+ routeUrl: String,
33
+ initialProps: Map<String, Any?>,
34
+ ): Intent {
35
+ return Intent(activity, NebulaActivity::class.java).apply {
36
+ putExtra(EXTRA_APP_ID, appId)
37
+ putExtra(EXTRA_ROUTE_PATH, routePath)
38
+ putExtra(EXTRA_ROUTE_URL, routeUrl)
39
+ putExtra(EXTRA_INITIAL_PROPS, org.json.JSONObject(initialProps).toString())
40
+ putExtra(EXTRA_INSTANCE_ID, UUID.randomUUID().toString())
41
+ }
42
+ }
43
+ }
44
+
45
+ lateinit var appId: String
46
+ private set
47
+
48
+ private lateinit var instanceId: String
49
+ private lateinit var routePath: String
50
+ private lateinit var routeUrl: String
51
+ private lateinit var rootHost: FrameLayout
52
+ private lateinit var toolbar: Toolbar
53
+ private var reactSurface: ReactSurface? = null
54
+ private var currentPageStyle: Map<String, Any?> = emptyMap()
55
+ private var didSignalContentReady = false
56
+ private var reactContentCreated = false
57
+
58
+ override fun onCreate(savedInstanceState: Bundle?) {
59
+ super.onCreate(savedInstanceState)
60
+ appId = intent.getStringExtra(EXTRA_APP_ID) ?: error("Missing appId")
61
+ routePath = NebulaManifestManager.normalizeRoute(intent.getStringExtra(EXTRA_ROUTE_PATH))
62
+ routeUrl = intent.getStringExtra(EXTRA_ROUTE_URL) ?: "nebula://$appId$routePath"
63
+ instanceId = intent.getStringExtra(EXTRA_INSTANCE_ID) ?: UUID.randomUUID().toString()
64
+
65
+ NebulaManifestManager.loadManifest(appId, NebulaConfig.sandboxDir(appId))
66
+
67
+ // Toolbar sits at the top; its top padding will be adjusted for the status bar inset.
68
+ toolbar = Toolbar(this).apply {
69
+ layoutParams = LinearLayout.LayoutParams(
70
+ LinearLayout.LayoutParams.MATCH_PARENT,
71
+ LinearLayout.LayoutParams.WRAP_CONTENT,
72
+ )
73
+ }
74
+
75
+ // rootHost fills the remaining space below the toolbar.
76
+ rootHost = FrameLayout(this).apply {
77
+ id = View.generateViewId()
78
+ layoutParams = LinearLayout.LayoutParams(
79
+ LinearLayout.LayoutParams.MATCH_PARENT,
80
+ 0,
81
+ 1f,
82
+ )
83
+ }
84
+
85
+ // A vertical LinearLayout guarantees rootHost is always below the toolbar
86
+ // without any manual margin/padding calculation.
87
+ val contentLayout = LinearLayout(this).apply {
88
+ orientation = LinearLayout.VERTICAL
89
+ layoutParams = LinearLayout.LayoutParams(
90
+ LinearLayout.LayoutParams.MATCH_PARENT,
91
+ LinearLayout.LayoutParams.MATCH_PARENT,
92
+ )
93
+ addView(toolbar)
94
+ addView(rootHost)
95
+ }
96
+
97
+ setSupportActionBar(toolbar)
98
+ supportActionBar?.setDisplayHomeAsUpEnabled(false)
99
+ toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
100
+
101
+ setContentView(contentLayout)
102
+
103
+ // Apply status-bar inset as top padding on the toolbar so its background
104
+ // extends behind the status bar (edge-to-edge).
105
+ ViewCompat.setOnApplyWindowInsetsListener(toolbar) { v, insets ->
106
+ val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
107
+ v.updatePadding(top = statusBarHeight)
108
+ insets
109
+ }
110
+
111
+ NebulaRouter.register(this)
112
+ updatePageStyle(NebulaManifestManager.getPageConfig(appId, routePath))
113
+ }
114
+
115
+ override fun onResume() {
116
+ super.onResume()
117
+ NebulaAppManager.onHostResume(appId, this, this)
118
+ if (!reactContentCreated) {
119
+ reactContentCreated = true
120
+ createReactContent()
121
+ }
122
+ NebulaEventHub.publishPageLifecycle(
123
+ NebulaPageLifecycleEvent(appId, instanceId, routePath, "show"),
124
+ )
125
+ }
126
+
127
+ override fun onPause() {
128
+ NebulaEventHub.publishPageLifecycle(
129
+ NebulaPageLifecycleEvent(appId, instanceId, routePath, "hide"),
130
+ )
131
+ NebulaAppManager.onHostPause(appId, this)
132
+ super.onPause()
133
+ }
134
+
135
+ override fun onDestroy() {
136
+ NebulaEventHub.publishPageLifecycle(
137
+ NebulaPageLifecycleEvent(appId, instanceId, routePath, "unload"),
138
+ )
139
+ reactSurface?.stop()
140
+ reactSurface?.clear()
141
+ reactSurface = null
142
+ NebulaAppManager.onHostDestroy(appId, this)
143
+ NebulaRouter.unregister(this)
144
+ super.onDestroy()
145
+ }
146
+
147
+ override fun invokeDefaultOnBackPressed() {
148
+ super.onBackPressed()
149
+ }
150
+
151
+ fun updatePageStyle(style: Map<String, Any?>) {
152
+ currentPageStyle = style
153
+ val backgroundColor =
154
+ parseColor(style["backgroundColor"] as? String)
155
+ ?: parseColor(style["navigationBarBackgroundColor"] as? String)
156
+ ?: Color.WHITE
157
+ rootHost.setBackgroundColor(backgroundColor)
158
+
159
+ val title = style["navigationBarTitleText"] as? String
160
+ supportActionBar?.title = title ?: appId
161
+
162
+ val navBackground = parseColor(style["navigationBarBackgroundColor"] as? String) ?: backgroundColor
163
+ supportActionBar?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(navBackground))
164
+
165
+ val textColor = parseColor(style["navigationBarTextColor"] as? String) ?: Color.BLACK
166
+ toolbar.setTitleTextColor(textColor)
167
+ toolbar.navigationIcon?.setTint(textColor)
168
+
169
+ val navigationStyle = (style["navigationStyle"] as? String)?.lowercase() ?: "default"
170
+ if (navigationStyle == "custom") {
171
+ supportActionBar?.hide()
172
+ } else {
173
+ supportActionBar?.show()
174
+ val shouldShowBackButton = !isEntryRoute()
175
+ supportActionBar?.setDisplayHomeAsUpEnabled(shouldShowBackButton)
176
+ supportActionBar?.setHomeButtonEnabled(shouldShowBackButton)
177
+ }
178
+ }
179
+
180
+ override fun onSupportNavigateUp(): Boolean {
181
+ onBackPressedDispatcher.onBackPressed()
182
+ return true
183
+ }
184
+
185
+ private fun isEntryRoute(): Boolean {
186
+ val entryRoute = NebulaManifestManager.normalizeRoute(
187
+ NebulaManifestManager.getEntryPagePath(appId),
188
+ )
189
+ return routePath == entryRoute
190
+ }
191
+
192
+ private fun createReactContent() {
193
+ val moduleName =
194
+ NebulaManifestManager.getComponentName(appId, routePath)
195
+ ?: NebulaManifestManager.getComponentName(appId, "/")
196
+ ?: "NebulaApp"
197
+ val initialProps = createInitialProps()
198
+ reactSurface = NebulaAppManager.createSurface(appId, this, moduleName, initialProps)
199
+ reactSurface?.start()
200
+ rootHost.removeAllViews()
201
+ reactSurface?.view?.let { view ->
202
+ rootHost.addView(
203
+ view,
204
+ FrameLayout.LayoutParams(
205
+ FrameLayout.LayoutParams.MATCH_PARENT,
206
+ FrameLayout.LayoutParams.MATCH_PARENT,
207
+ ),
208
+ )
209
+ registerFirstContentDrawListener(view)
210
+ }
211
+ }
212
+
213
+ private fun registerFirstContentDrawListener(view: View) {
214
+ val observer = view.viewTreeObserver
215
+ if (!observer.isAlive) {
216
+ handleMiniappContentReady()
217
+ return
218
+ }
219
+
220
+ observer.addOnPreDrawListener(
221
+ object : ViewTreeObserver.OnPreDrawListener {
222
+ override fun onPreDraw(): Boolean {
223
+ if (view.viewTreeObserver.isAlive) {
224
+ view.viewTreeObserver.removeOnPreDrawListener(this)
225
+ }
226
+ handleMiniappContentReady()
227
+ return true
228
+ }
229
+ },
230
+ )
231
+ }
232
+
233
+ private fun handleMiniappContentReady() {
234
+ if (didSignalContentReady) {
235
+ return
236
+ }
237
+ didSignalContentReady = true
238
+ NebulaEventHub.publishPageLifecycle(
239
+ NebulaPageLifecycleEvent(appId, instanceId, routePath, "ready"),
240
+ )
241
+ NebulaRouter.onMiniappContentReady(appId)
242
+ }
243
+
244
+ private fun createInitialProps(): Bundle {
245
+ val props = parseJsonMap(intent.getStringExtra(EXTRA_INITIAL_PROPS))
246
+ val pageStyle = NebulaManifestManager.getPageConfig(appId, routePath)
247
+ props["appId"] = appId
248
+ props["instanceId"] = instanceId
249
+ props["sandboxPath"] = NebulaConfig.sandboxDir(appId).absolutePath
250
+ props["__routePath"] = routePath
251
+ props["__routeUrl"] = routeUrl
252
+ props["__pageConfig"] = pageStyle
253
+ return props.toBundle()
254
+ }
255
+
256
+ private fun parseColor(color: String?): Int? {
257
+ if (color.isNullOrBlank()) {
258
+ return null
259
+ }
260
+ return runCatching { Color.parseColor(color) }.getOrNull()
261
+ }
262
+
263
+ private fun parseJsonMap(raw: String?): MutableMap<String, Any?> {
264
+ if (raw.isNullOrBlank()) {
265
+ return linkedMapOf()
266
+ }
267
+ return NebulaManifestManager.jsonObjectToMap(org.json.JSONObject(raw)).toMutableMap()
268
+ }
269
+ }
270
+
271
+ private fun Map<String, Any?>.toBundle(): Bundle {
272
+ val bundle = Bundle()
273
+ forEach { (key, value) ->
274
+ when (value) {
275
+ null -> bundle.putString(key, null)
276
+ is String -> bundle.putString(key, value)
277
+ is Int -> bundle.putInt(key, value)
278
+ is Double -> bundle.putDouble(key, value)
279
+ is Boolean -> bundle.putBoolean(key, value)
280
+ is Float -> bundle.putFloat(key, value)
281
+ is Long -> bundle.putLong(key, value)
282
+ is Map<*, *> -> {
283
+ @Suppress("UNCHECKED_CAST")
284
+ bundle.putBundle(key, (value as Map<String, Any?>).toBundle())
285
+ }
286
+ else -> bundle.putString(key, value.toString())
287
+ }
288
+ }
289
+ return bundle
290
+ }
@@ -0,0 +1,134 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.os.Bundle
6
+ import com.facebook.react.ReactHost
7
+ import com.facebook.react.ReactInstanceEventListener
8
+ import com.facebook.react.ReactPackage
9
+ import com.facebook.react.defaults.DefaultComponentsRegistry
10
+ import com.facebook.react.defaults.DefaultReactHostDelegate
11
+ import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
12
+ import com.facebook.react.fabric.ComponentFactory
13
+ import com.facebook.react.interfaces.fabric.ReactSurface
14
+ import com.facebook.react.runtime.ReactHostImpl
15
+ import com.facebook.react.runtime.hermes.HermesInstance
16
+ import com.facebook.react.bridge.JSBundleLoader
17
+ import java.util.concurrent.ConcurrentHashMap
18
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
19
+
20
+ @OptIn(UnstableReactNativeAPI::class)
21
+ object NebulaAppManager {
22
+ private lateinit var application: Application
23
+ private var delegate: NebulaHostDelegate? = null
24
+ private val hosts = ConcurrentHashMap<String, ReactHost>()
25
+
26
+ fun initialize(app: Application, delegate: NebulaHostDelegate? = null) {
27
+ application = app
28
+ this.delegate = delegate
29
+ }
30
+
31
+ fun preload(appId: String) {
32
+ acquireHost(appId)
33
+ }
34
+
35
+ fun startHost(appId: String, onReady: (() -> Unit)? = null) {
36
+ val host = acquireHost(appId)
37
+ if (onReady == null) {
38
+ host.start()
39
+ return
40
+ }
41
+ if (host.currentReactContext != null) {
42
+ android.os.Handler(android.os.Looper.getMainLooper()).post(onReady)
43
+ return
44
+ }
45
+ val listener = object : ReactInstanceEventListener {
46
+ override fun onReactContextInitialized(context: com.facebook.react.bridge.ReactContext) {
47
+ host.removeReactInstanceEventListener(this)
48
+ onReady()
49
+ }
50
+ }
51
+ host.addReactInstanceEventListener(listener)
52
+ host.start()
53
+ }
54
+
55
+ fun invalidate(appId: String) {
56
+ hosts.remove(appId)?.let { host ->
57
+ if (host is ReactHostImpl) {
58
+ host.destroy("NebulaAppManager.invalidate($appId)", null)
59
+ }
60
+ }
61
+ }
62
+
63
+ fun invalidateAll() {
64
+ hosts.forEach { (_, host) ->
65
+ if (host is ReactHostImpl) {
66
+ host.destroy("NebulaAppManager.invalidateAll", null)
67
+ }
68
+ }
69
+ hosts.clear()
70
+ }
71
+
72
+ fun acquireHost(appId: String): ReactHost {
73
+ hosts[appId]?.let { return it }
74
+
75
+ val installedApp =
76
+ NebulaConfig.getInstalledApp(appId)
77
+ ?: error("Mini-app $appId is not installed")
78
+ val enableDevSupport =
79
+ NebulaHost.isDebugBuild() && NebulaConfig.isDevelopmentMode(installedApp)
80
+ val devServerHost = NebulaConfig.getDevServerHost(installedApp)
81
+ val jsMainModulePath =
82
+ NebulaConfig.getDevModulePath(installedApp) ?: "index"
83
+
84
+ val packages = (delegate?.createMiniAppPackages(application) ?: emptyList()).toMutableList()
85
+ packages.add(NebulaPackage())
86
+
87
+ val delegate =
88
+ DefaultReactHostDelegate(
89
+ jsMainModulePath = jsMainModulePath,
90
+ jsBundleLoader = JSBundleLoader.createFileLoader(installedApp.bundlePath),
91
+ reactPackages = packages.distinctBy { it.javaClass.name },
92
+ jsRuntimeFactory = HermesInstance(),
93
+ turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
94
+ )
95
+
96
+ val componentFactory = ComponentFactory().also { DefaultComponentsRegistry.register(it) }
97
+ val host =
98
+ ReactHostImpl(
99
+ application,
100
+ delegate,
101
+ componentFactory,
102
+ enableDevSupport,
103
+ enableDevSupport,
104
+ )
105
+
106
+ if (enableDevSupport && !devServerHost.isNullOrBlank()) {
107
+ host.setBundleSource(devServerHost, jsMainModulePath)
108
+ }
109
+
110
+ hosts[appId] = host
111
+ return host
112
+ }
113
+
114
+ fun createSurface(appId: String, activity: Activity, moduleName: String, initialProps: Bundle): ReactSurface {
115
+ val host = acquireHost(appId)
116
+ return host.createSurface(activity, moduleName, initialProps)
117
+ }
118
+
119
+ fun onHostPause(appId: String, activity: Activity) {
120
+ hosts[appId]?.onHostPause(activity)
121
+ }
122
+
123
+ fun onHostResume(appId: String, activity: Activity) {
124
+ hosts[appId]?.onHostResume(activity)
125
+ }
126
+
127
+ fun onHostResume(appId: String, activity: Activity, backButtonHandler: com.facebook.react.modules.core.DefaultHardwareBackBtnHandler) {
128
+ hosts[appId]?.onHostResume(activity, backButtonHandler)
129
+ }
130
+
131
+ fun onHostDestroy(appId: String, activity: Activity) {
132
+ hosts[appId]?.onHostDestroy(activity)
133
+ }
134
+ }