@onekeyfe/react-native-split-bundle-loader 0.1.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.
- package/SplitBundleLoader.podspec +22 -0
- package/android/build.gradle +77 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/splitbundleloader/SBLLogger.kt +55 -0
- package/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt +199 -0
- package/android/src/main/java/com/splitbundleloader/SplitBundleLoaderPackage.kt +33 -0
- package/ios/SBLLogger.h +16 -0
- package/ios/SBLLogger.m +42 -0
- package/ios/SplitBundleLoader.h +14 -0
- package/ios/SplitBundleLoader.mm +149 -0
- package/lib/module/NativeSplitBundleLoader.js +5 -0
- package/lib/module/NativeSplitBundleLoader.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSplitBundleLoader.d.ts +15 -0
- package/lib/typescript/src/NativeSplitBundleLoader.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +162 -0
- package/src/NativeSplitBundleLoader.ts +22 -0
- package/src/index.tsx +4 -0
|
@@ -0,0 +1,22 @@
|
|
|
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 = "SplitBundleLoader"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-split-bundle-loader.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.public_header_files = "ios/SBLLogger.h"
|
|
18
|
+
|
|
19
|
+
s.dependency 'ReactNativeNativeLogger'
|
|
20
|
+
|
|
21
|
+
install_modules_dependencies(s)
|
|
22
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['SplitBundleLoader_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
|
|
24
|
+
def getExtOrIntegerDefault(name) {
|
|
25
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["SplitBundleLoader_" + name]).toInteger()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace "com.splitbundleloader"
|
|
30
|
+
|
|
31
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
32
|
+
|
|
33
|
+
defaultConfig {
|
|
34
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
35
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildFeatures {
|
|
39
|
+
buildConfig true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildTypes {
|
|
43
|
+
release {
|
|
44
|
+
minifyEnabled false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lintOptions {
|
|
49
|
+
disable "GradleCompatible"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
compileOptions {
|
|
53
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
54
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sourceSets {
|
|
58
|
+
main {
|
|
59
|
+
java.srcDirs += [
|
|
60
|
+
"generated/java",
|
|
61
|
+
"generated/jni"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
repositories {
|
|
68
|
+
mavenCentral()
|
|
69
|
+
google()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
73
|
+
|
|
74
|
+
dependencies {
|
|
75
|
+
implementation "com.facebook.react:react-android"
|
|
76
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
77
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.splitbundleloader
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight logging wrapper that dynamically dispatches to OneKeyLog.
|
|
5
|
+
* Uses reflection to avoid a hard dependency on the native-logger module.
|
|
6
|
+
* Falls back to android.util.Log when OneKeyLog is not available.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors iOS SBLLogger.
|
|
9
|
+
*/
|
|
10
|
+
object SBLLogger {
|
|
11
|
+
private const val TAG = "SplitBundleLoader"
|
|
12
|
+
|
|
13
|
+
private val logClass: Class<*>? by lazy {
|
|
14
|
+
try {
|
|
15
|
+
Class.forName("com.margelo.nitro.nativelogger.OneKeyLog")
|
|
16
|
+
} catch (_: ClassNotFoundException) {
|
|
17
|
+
null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private val methods by lazy {
|
|
22
|
+
val cls = logClass ?: return@lazy null
|
|
23
|
+
mapOf(
|
|
24
|
+
"debug" to cls.getMethod("debug", String::class.java, String::class.java),
|
|
25
|
+
"info" to cls.getMethod("info", String::class.java, String::class.java),
|
|
26
|
+
"warn" to cls.getMethod("warn", String::class.java, String::class.java),
|
|
27
|
+
"error" to cls.getMethod("error", String::class.java, String::class.java),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@JvmStatic
|
|
32
|
+
fun debug(message: String) = log("debug", message, android.util.Log.DEBUG)
|
|
33
|
+
|
|
34
|
+
@JvmStatic
|
|
35
|
+
fun info(message: String) = log("info", message, android.util.Log.INFO)
|
|
36
|
+
|
|
37
|
+
@JvmStatic
|
|
38
|
+
fun warn(message: String) = log("warn", message, android.util.Log.WARN)
|
|
39
|
+
|
|
40
|
+
@JvmStatic
|
|
41
|
+
fun error(message: String) = log("error", message, android.util.Log.ERROR)
|
|
42
|
+
|
|
43
|
+
private fun log(level: String, message: String, androidLogLevel: Int) {
|
|
44
|
+
val method = methods?.get(level)
|
|
45
|
+
if (method != null) {
|
|
46
|
+
try {
|
|
47
|
+
method.invoke(null, TAG, message)
|
|
48
|
+
return
|
|
49
|
+
} catch (_: Exception) {
|
|
50
|
+
// Fall through to android.util.Log
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
android.util.Log.println(androidLogLevel, TAG, message)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.splitbundleloader
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.AssetManager
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
9
|
+
import java.io.File
|
|
10
|
+
import java.io.FileOutputStream
|
|
11
|
+
import java.io.IOException
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TurboModule entry point for SplitBundleLoader.
|
|
15
|
+
*
|
|
16
|
+
* Provides two methods to JS:
|
|
17
|
+
* 1. getRuntimeBundleContext() — Returns current runtime's bundle paths and source kind
|
|
18
|
+
* 2. loadSegment(params) — Registers a HBC segment with the current Hermes runtime
|
|
19
|
+
*
|
|
20
|
+
* Mirrors iOS SplitBundleLoader.mm.
|
|
21
|
+
*/
|
|
22
|
+
@ReactModule(name = SplitBundleLoaderModule.NAME)
|
|
23
|
+
class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
24
|
+
NativeSplitBundleLoaderSpec(reactContext) {
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
const val NAME = "SplitBundleLoader"
|
|
28
|
+
private const val BUILTIN_EXTRACT_DIR = "onekey-builtin-segments"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun getName(): String = NAME
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
// getRuntimeBundleContext
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
override fun getRuntimeBundleContext(promise: Promise) {
|
|
38
|
+
try {
|
|
39
|
+
val context = reactApplicationContext
|
|
40
|
+
val runtimeKind = "main"
|
|
41
|
+
var sourceKind = "builtin"
|
|
42
|
+
var bundleRoot = ""
|
|
43
|
+
var nativeVersion = ""
|
|
44
|
+
val bundleVersion = ""
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
nativeVersion = context.packageManager
|
|
48
|
+
.getPackageInfo(context.packageName, 0).versionName ?: ""
|
|
49
|
+
} catch (_: Exception) {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check OTA bundle path
|
|
53
|
+
val otaBundlePath = getOtaBundlePath()
|
|
54
|
+
if (!otaBundlePath.isNullOrEmpty()) {
|
|
55
|
+
val otaFile = File(otaBundlePath)
|
|
56
|
+
if (otaFile.exists()) {
|
|
57
|
+
sourceKind = "ota"
|
|
58
|
+
bundleRoot = otaFile.parent ?: ""
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
val builtinExtractRoot = File(
|
|
63
|
+
context.filesDir,
|
|
64
|
+
"$BUILTIN_EXTRACT_DIR/$nativeVersion"
|
|
65
|
+
).absolutePath
|
|
66
|
+
|
|
67
|
+
val result = Arguments.createMap()
|
|
68
|
+
result.putString("runtimeKind", runtimeKind)
|
|
69
|
+
result.putString("sourceKind", sourceKind)
|
|
70
|
+
result.putString("bundleRoot", bundleRoot)
|
|
71
|
+
result.putString("builtinExtractRoot", builtinExtractRoot)
|
|
72
|
+
result.putString("nativeVersion", nativeVersion)
|
|
73
|
+
result.putString("bundleVersion", bundleVersion)
|
|
74
|
+
|
|
75
|
+
promise.resolve(result)
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
promise.reject("SPLIT_BUNDLE_CONTEXT_ERROR", e.message, e)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
// loadSegment
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
override fun loadSegment(
|
|
86
|
+
segmentId: Double,
|
|
87
|
+
segmentKey: String,
|
|
88
|
+
relativePath: String,
|
|
89
|
+
sha256: String,
|
|
90
|
+
promise: Promise
|
|
91
|
+
) {
|
|
92
|
+
try {
|
|
93
|
+
val segId = segmentId.toInt()
|
|
94
|
+
|
|
95
|
+
val absolutePath = resolveSegmentPath(relativePath)
|
|
96
|
+
if (absolutePath == null) {
|
|
97
|
+
promise.reject(
|
|
98
|
+
"SPLIT_BUNDLE_NOT_FOUND",
|
|
99
|
+
"Segment file not found: $relativePath (key=$segmentKey)"
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register segment via CatalystInstance
|
|
105
|
+
val reactContext = reactApplicationContext
|
|
106
|
+
if (reactContext.hasCatalystInstance()) {
|
|
107
|
+
reactContext.catalystInstance.registerSegment(segId, absolutePath)
|
|
108
|
+
SBLLogger.info("Loaded segment $segmentKey (id=$segId)")
|
|
109
|
+
promise.resolve(null)
|
|
110
|
+
} else {
|
|
111
|
+
promise.reject(
|
|
112
|
+
"SPLIT_BUNDLE_NO_INSTANCE",
|
|
113
|
+
"CatalystInstance not available"
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
promise.reject("SPLIT_BUNDLE_LOAD_ERROR", e.message, e)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -----------------------------------------------------------------------
|
|
122
|
+
// Path resolution helpers
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
private fun resolveSegmentPath(relativePath: String): String? {
|
|
126
|
+
// 1. Try OTA bundle directory first
|
|
127
|
+
val otaBundlePath = getOtaBundlePath()
|
|
128
|
+
if (!otaBundlePath.isNullOrEmpty()) {
|
|
129
|
+
val otaRoot = File(otaBundlePath).parentFile
|
|
130
|
+
if (otaRoot != null) {
|
|
131
|
+
val candidate = File(otaRoot, relativePath)
|
|
132
|
+
if (candidate.exists()) {
|
|
133
|
+
return candidate.absolutePath
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. Try builtin: extract from assets if needed
|
|
139
|
+
return extractBuiltinSegmentIfNeeded(relativePath)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* For Android builtin segments, APK assets can't be passed directly as file paths.
|
|
144
|
+
* Extract the asset to the extract cache directory on first access.
|
|
145
|
+
*/
|
|
146
|
+
private fun extractBuiltinSegmentIfNeeded(relativePath: String): String? {
|
|
147
|
+
val context = reactApplicationContext
|
|
148
|
+
val nativeVersion = try {
|
|
149
|
+
context.packageManager
|
|
150
|
+
.getPackageInfo(context.packageName, 0).versionName ?: "unknown"
|
|
151
|
+
} catch (_: Exception) {
|
|
152
|
+
"unknown"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val extractDir = File(context.filesDir, "$BUILTIN_EXTRACT_DIR/$nativeVersion")
|
|
156
|
+
val extractedFile = File(extractDir, relativePath)
|
|
157
|
+
|
|
158
|
+
// Already extracted
|
|
159
|
+
if (extractedFile.exists()) {
|
|
160
|
+
return extractedFile.absolutePath
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Extract from assets
|
|
164
|
+
val assets: AssetManager = context.assets
|
|
165
|
+
return try {
|
|
166
|
+
assets.open(relativePath).use { input ->
|
|
167
|
+
extractedFile.parentFile?.let { parent ->
|
|
168
|
+
if (!parent.exists()) parent.mkdirs()
|
|
169
|
+
}
|
|
170
|
+
FileOutputStream(extractedFile).use { output ->
|
|
171
|
+
val buffer = ByteArray(8192)
|
|
172
|
+
var len: Int
|
|
173
|
+
while (input.read(buffer).also { len = it } != -1) {
|
|
174
|
+
output.write(buffer, 0, len)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
extractedFile.absolutePath
|
|
179
|
+
} catch (_: IOException) {
|
|
180
|
+
null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun getOtaBundlePath(): String? {
|
|
185
|
+
return try {
|
|
186
|
+
val bundleUpdateStore = Class.forName(
|
|
187
|
+
"expo.modules.onekeybundleupdate.BundleUpdateStore"
|
|
188
|
+
)
|
|
189
|
+
val method = bundleUpdateStore.getMethod(
|
|
190
|
+
"getCurrentBundleMainJSBundle",
|
|
191
|
+
Context::class.java
|
|
192
|
+
)
|
|
193
|
+
val result = method.invoke(null, reactApplicationContext)
|
|
194
|
+
result?.toString()
|
|
195
|
+
} catch (_: Exception) {
|
|
196
|
+
null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.splitbundleloader
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class SplitBundleLoaderPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == SplitBundleLoaderModule.NAME) {
|
|
13
|
+
SplitBundleLoaderModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[SplitBundleLoaderModule.NAME] = ReactModuleInfo(
|
|
23
|
+
SplitBundleLoaderModule.NAME,
|
|
24
|
+
SplitBundleLoaderModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/ios/SBLLogger.h
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Lightweight logging wrapper that dynamically dispatches to OneKeyLog.
|
|
6
|
+
/// Avoids `@import ReactNativeNativeLogger` which fails in .mm (Objective-C++) files.
|
|
7
|
+
@interface SBLLogger : NSObject
|
|
8
|
+
|
|
9
|
+
+ (void)debug:(NSString *)message;
|
|
10
|
+
+ (void)info:(NSString *)message;
|
|
11
|
+
+ (void)warn:(NSString *)message;
|
|
12
|
+
+ (void)error:(NSString *)message;
|
|
13
|
+
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
NS_ASSUME_NONNULL_END
|
package/ios/SBLLogger.m
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#import "SBLLogger.h"
|
|
2
|
+
|
|
3
|
+
static NSString *const kTag = @"SplitBundleLoader";
|
|
4
|
+
|
|
5
|
+
@implementation SBLLogger
|
|
6
|
+
|
|
7
|
+
+ (void)debug:(NSString *)message {
|
|
8
|
+
[self _log:@"debug::" message:message];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
+ (void)info:(NSString *)message {
|
|
12
|
+
[self _log:@"info::" message:message];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
+ (void)warn:(NSString *)message {
|
|
16
|
+
[self _log:@"warn::" message:message];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
+ (void)error:(NSString *)message {
|
|
20
|
+
[self _log:@"error::" message:message];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#pragma mark - Private
|
|
24
|
+
|
|
25
|
+
+ (void)_log:(NSString *)selectorName message:(NSString *)message {
|
|
26
|
+
Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
|
|
27
|
+
if (!logClass) {
|
|
28
|
+
logClass = NSClassFromString(@"OneKeyLog");
|
|
29
|
+
}
|
|
30
|
+
if (!logClass) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
SEL sel = NSSelectorFromString(selectorName);
|
|
34
|
+
if (![logClass respondsToSelector:sel]) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
typedef void (*LogFunc)(id, SEL, NSString *, NSString *);
|
|
38
|
+
LogFunc func = (LogFunc)[logClass methodForSelector:sel];
|
|
39
|
+
func(logClass, sel, kTag, message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <SplitBundleLoaderSpec/SplitBundleLoaderSpec.h>
|
|
2
|
+
|
|
3
|
+
@interface SplitBundleLoader : NativeSplitBundleLoaderSpecBase <NativeSplitBundleLoaderSpec>
|
|
4
|
+
|
|
5
|
+
- (void)getRuntimeBundleContext:(RCTPromiseResolveBlock)resolve
|
|
6
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
7
|
+
- (void)loadSegment:(double)segmentId
|
|
8
|
+
segmentKey:(NSString *)segmentKey
|
|
9
|
+
relativePath:(NSString *)relativePath
|
|
10
|
+
sha256:(NSString *)sha256
|
|
11
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
13
|
+
|
|
14
|
+
@end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#import "SplitBundleLoader.h"
|
|
2
|
+
#import "SBLLogger.h"
|
|
3
|
+
#import <React/RCTBridge.h>
|
|
4
|
+
|
|
5
|
+
@implementation SplitBundleLoader
|
|
6
|
+
|
|
7
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
8
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
9
|
+
{
|
|
10
|
+
[SBLLogger info:@"SplitBundleLoader module initialized"];
|
|
11
|
+
return std::make_shared<facebook::react::NativeSplitBundleLoaderSpecJSI>(params);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
+ (NSString *)moduleName
|
|
15
|
+
{
|
|
16
|
+
return @"SplitBundleLoader";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
+ (BOOL)requiresMainQueueSetup
|
|
20
|
+
{
|
|
21
|
+
return NO;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// MARK: - getRuntimeBundleContext
|
|
25
|
+
|
|
26
|
+
- (void)getRuntimeBundleContext:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
28
|
+
{
|
|
29
|
+
@try {
|
|
30
|
+
NSString *runtimeKind = @"main";
|
|
31
|
+
NSString *sourceKind = @"builtin";
|
|
32
|
+
NSString *bundleRoot = @"";
|
|
33
|
+
NSString *nativeVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"";
|
|
34
|
+
NSString *bundleVersion = @"";
|
|
35
|
+
|
|
36
|
+
// Check if OTA bundle is active via BundleUpdateStore
|
|
37
|
+
Class bundleUpdateStoreClass = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
|
|
38
|
+
if (bundleUpdateStoreClass) {
|
|
39
|
+
NSString *otaBundlePath = nil;
|
|
40
|
+
SEL sel = NSSelectorFromString(@"currentBundleMainJSBundle");
|
|
41
|
+
if ([bundleUpdateStoreClass respondsToSelector:sel]) {
|
|
42
|
+
#pragma clang diagnostic push
|
|
43
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
44
|
+
id result = [bundleUpdateStoreClass performSelector:sel];
|
|
45
|
+
#pragma clang diagnostic pop
|
|
46
|
+
otaBundlePath = [result isKindOfClass:[NSString class]] ? (NSString *)result : nil;
|
|
47
|
+
}
|
|
48
|
+
if (otaBundlePath && otaBundlePath.length > 0) {
|
|
49
|
+
NSString *filePath = otaBundlePath;
|
|
50
|
+
if ([otaBundlePath hasPrefix:@"file://"]) {
|
|
51
|
+
filePath = [[NSURL URLWithString:otaBundlePath] path];
|
|
52
|
+
}
|
|
53
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
|
54
|
+
sourceKind = @"ota";
|
|
55
|
+
bundleRoot = [filePath stringByDeletingLastPathComponent];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Builtin: use main bundle resource path
|
|
61
|
+
if ([sourceKind isEqualToString:@"builtin"]) {
|
|
62
|
+
bundleRoot = [[NSBundle mainBundle] resourcePath] ?: @"";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolve(@{
|
|
66
|
+
@"runtimeKind": runtimeKind,
|
|
67
|
+
@"sourceKind": sourceKind,
|
|
68
|
+
@"bundleRoot": bundleRoot,
|
|
69
|
+
@"nativeVersion": nativeVersion,
|
|
70
|
+
@"bundleVersion": bundleVersion,
|
|
71
|
+
});
|
|
72
|
+
} @catch (NSException *exception) {
|
|
73
|
+
reject(@"SPLIT_BUNDLE_CONTEXT_ERROR", exception.reason, nil);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - loadSegment
|
|
78
|
+
|
|
79
|
+
- (void)loadSegment:(double)segmentId
|
|
80
|
+
segmentKey:(NSString *)segmentKey
|
|
81
|
+
relativePath:(NSString *)relativePath
|
|
82
|
+
sha256:(NSString *)sha256
|
|
83
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
84
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
85
|
+
{
|
|
86
|
+
@try {
|
|
87
|
+
int segId = (int)segmentId;
|
|
88
|
+
|
|
89
|
+
// Resolve absolute path
|
|
90
|
+
NSString *absolutePath = nil;
|
|
91
|
+
|
|
92
|
+
// 1. Try OTA bundle root first
|
|
93
|
+
Class bundleUpdateStoreClass = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
|
|
94
|
+
if (bundleUpdateStoreClass) {
|
|
95
|
+
NSString *otaBundlePath = nil;
|
|
96
|
+
SEL sel = NSSelectorFromString(@"currentBundleMainJSBundle");
|
|
97
|
+
if ([bundleUpdateStoreClass respondsToSelector:sel]) {
|
|
98
|
+
#pragma clang diagnostic push
|
|
99
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
100
|
+
id result = [bundleUpdateStoreClass performSelector:sel];
|
|
101
|
+
#pragma clang diagnostic pop
|
|
102
|
+
otaBundlePath = [result isKindOfClass:[NSString class]] ? (NSString *)result : nil;
|
|
103
|
+
}
|
|
104
|
+
if (otaBundlePath && otaBundlePath.length > 0) {
|
|
105
|
+
NSString *filePath = otaBundlePath;
|
|
106
|
+
if ([otaBundlePath hasPrefix:@"file://"]) {
|
|
107
|
+
filePath = [[NSURL URLWithString:otaBundlePath] path];
|
|
108
|
+
}
|
|
109
|
+
NSString *otaRoot = [filePath stringByDeletingLastPathComponent];
|
|
110
|
+
NSString *candidate = [otaRoot stringByAppendingPathComponent:relativePath];
|
|
111
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
112
|
+
absolutePath = candidate;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Fallback to builtin resource path
|
|
118
|
+
if (!absolutePath) {
|
|
119
|
+
NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
|
|
120
|
+
NSString *candidate = [builtinRoot stringByAppendingPathComponent:relativePath];
|
|
121
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
122
|
+
absolutePath = candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!absolutePath) {
|
|
127
|
+
reject(@"SPLIT_BUNDLE_NOT_FOUND",
|
|
128
|
+
[NSString stringWithFormat:@"Segment file not found: %@ (key=%@)", relativePath, segmentKey],
|
|
129
|
+
nil);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Register segment via RCTBridge
|
|
134
|
+
RCTBridge *bridge = [RCTBridge currentBridge];
|
|
135
|
+
if (bridge) {
|
|
136
|
+
[bridge registerSegmentWithId:@(segId) path:absolutePath];
|
|
137
|
+
[SBLLogger info:[NSString stringWithFormat:@"Loaded segment %@ (id=%d)", segmentKey, segId]];
|
|
138
|
+
resolve(nil);
|
|
139
|
+
} else {
|
|
140
|
+
reject(@"SPLIT_BUNDLE_NO_BRIDGE", @"RCTBridge not available", nil);
|
|
141
|
+
}
|
|
142
|
+
} @catch (NSException *exception) {
|
|
143
|
+
reject(@"SPLIT_BUNDLE_LOAD_ERROR",
|
|
144
|
+
[NSString stringWithFormat:@"Failed to load segment %@: %@", segmentKey, exception.reason],
|
|
145
|
+
nil);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSplitBundleLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAqBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeSplitBundleLoader","SplitBundleLoader"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,uBAAuB,MAAM,8BAA2B;AAE/D,OAAO,MAAMC,iBAAiB,GAAGD,uBAAuB","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
getRuntimeBundleContext(): Promise<{
|
|
4
|
+
runtimeKind: string;
|
|
5
|
+
sourceKind: string;
|
|
6
|
+
bundleRoot: string;
|
|
7
|
+
builtinExtractRoot?: string;
|
|
8
|
+
nativeVersion: string;
|
|
9
|
+
bundleVersion?: string;
|
|
10
|
+
}>;
|
|
11
|
+
loadSegment(segmentId: number, segmentKey: string, relativePath: string, sha256: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
declare const _default: Spec;
|
|
14
|
+
export default _default;
|
|
15
|
+
//# sourceMappingURL=NativeSplitBundleLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeSplitBundleLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeSplitBundleLoader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,IAAI,OAAO,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;;AAED,wBAA2E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,0CAA0B,CAAC;AACzD,YAAY,EAAE,IAAI,IAAI,qBAAqB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onekeyfe/react-native-split-bundle-loader",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "react-native-split-bundle-loader",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"*.podspec",
|
|
21
|
+
"!ios/build",
|
|
22
|
+
"!android/build",
|
|
23
|
+
"!android/gradle",
|
|
24
|
+
"!android/gradlew",
|
|
25
|
+
"!android/gradlew.bat",
|
|
26
|
+
"!android/local.properties",
|
|
27
|
+
"!**/__tests__",
|
|
28
|
+
"!**/__fixtures__",
|
|
29
|
+
"!**/__mocks__",
|
|
30
|
+
"!**/.*"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
34
|
+
"prepare": "bob build",
|
|
35
|
+
"typecheck": "tsc",
|
|
36
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
37
|
+
"test": "jest",
|
|
38
|
+
"release": "yarn prepare && npm whoami && npm publish --access public"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react-native",
|
|
42
|
+
"ios",
|
|
43
|
+
"android"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/OneKeyHQ/app-modules/react-native-split-bundle-loader.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "@onekeyhq <huanming@onekey.so> (https://github.com/OneKeyHQ/app-modules)",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/OneKeyHQ/app-modules/react-native-split-bundle-loader/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/OneKeyHQ/app-modules/react-native-split-bundle-loader#readme",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
60
|
+
"@eslint/compat": "^1.3.2",
|
|
61
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
62
|
+
"@eslint/js": "^9.35.0",
|
|
63
|
+
"@react-native/babel-preset": "0.83.0",
|
|
64
|
+
"@react-native/eslint-config": "0.83.0",
|
|
65
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
66
|
+
"@types/jest": "^29.5.14",
|
|
67
|
+
"@types/react": "^19.2.0",
|
|
68
|
+
"commitlint": "^19.8.1",
|
|
69
|
+
"del-cli": "^6.0.0",
|
|
70
|
+
"eslint": "^9.35.0",
|
|
71
|
+
"eslint-config-prettier": "^10.1.8",
|
|
72
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
73
|
+
"jest": "^29.7.0",
|
|
74
|
+
"lefthook": "^2.0.3",
|
|
75
|
+
"prettier": "^2.8.8",
|
|
76
|
+
"react": "19.2.0",
|
|
77
|
+
"react-native": "patch:react-native@npm%3A0.83.0#~/.yarn/patches/react-native-npm-0.83.0-577d0f2d83.patch",
|
|
78
|
+
"react-native-builder-bob": "^0.40.17",
|
|
79
|
+
"release-it": "^19.0.4",
|
|
80
|
+
"turbo": "^2.5.6",
|
|
81
|
+
"typescript": "^5.9.2"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"react": "*",
|
|
85
|
+
"react-native": "*"
|
|
86
|
+
},
|
|
87
|
+
"react-native-builder-bob": {
|
|
88
|
+
"source": "src",
|
|
89
|
+
"output": "lib",
|
|
90
|
+
"targets": [
|
|
91
|
+
[
|
|
92
|
+
"module",
|
|
93
|
+
{
|
|
94
|
+
"esm": true
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"typescript",
|
|
99
|
+
{
|
|
100
|
+
"project": "tsconfig.build.json"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"codegenConfig": {
|
|
106
|
+
"name": "SplitBundleLoaderSpec",
|
|
107
|
+
"type": "modules",
|
|
108
|
+
"jsSrcsDir": "src",
|
|
109
|
+
"android": {
|
|
110
|
+
"javaPackageName": "com.splitbundleloader"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"prettier": {
|
|
114
|
+
"quoteProps": "consistent",
|
|
115
|
+
"singleQuote": true,
|
|
116
|
+
"tabWidth": 2,
|
|
117
|
+
"trailingComma": "es5",
|
|
118
|
+
"useTabs": false
|
|
119
|
+
},
|
|
120
|
+
"jest": {
|
|
121
|
+
"preset": "react-native",
|
|
122
|
+
"modulePathIgnorePatterns": [
|
|
123
|
+
"<rootDir>/example/node_modules",
|
|
124
|
+
"<rootDir>/lib/"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"commitlint": {
|
|
128
|
+
"extends": [
|
|
129
|
+
"@commitlint/config-conventional"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"release-it": {
|
|
133
|
+
"git": {
|
|
134
|
+
"commitMessage": "chore: release ${version}",
|
|
135
|
+
"tagName": "v${version}"
|
|
136
|
+
},
|
|
137
|
+
"npm": {
|
|
138
|
+
"publish": true
|
|
139
|
+
},
|
|
140
|
+
"github": {
|
|
141
|
+
"release": true
|
|
142
|
+
},
|
|
143
|
+
"plugins": {
|
|
144
|
+
"@release-it/conventional-changelog": {
|
|
145
|
+
"preset": {
|
|
146
|
+
"name": "angular"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"create-react-native-library": {
|
|
152
|
+
"type": "turbo-module",
|
|
153
|
+
"languages": "kotlin-objc",
|
|
154
|
+
"tools": [
|
|
155
|
+
"eslint",
|
|
156
|
+
"jest",
|
|
157
|
+
"lefthook",
|
|
158
|
+
"release-it"
|
|
159
|
+
],
|
|
160
|
+
"version": "0.56.0"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { TurboModule } from 'react-native';
|
|
4
|
+
|
|
5
|
+
export interface Spec extends TurboModule {
|
|
6
|
+
getRuntimeBundleContext(): Promise<{
|
|
7
|
+
runtimeKind: string;
|
|
8
|
+
sourceKind: string;
|
|
9
|
+
bundleRoot: string;
|
|
10
|
+
builtinExtractRoot?: string;
|
|
11
|
+
nativeVersion: string;
|
|
12
|
+
bundleVersion?: string;
|
|
13
|
+
}>;
|
|
14
|
+
loadSegment(
|
|
15
|
+
segmentId: number,
|
|
16
|
+
segmentKey: string,
|
|
17
|
+
relativePath: string,
|
|
18
|
+
sha256: string,
|
|
19
|
+
): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('SplitBundleLoader');
|
package/src/index.tsx
ADDED