react-native-litert-lm 0.3.7 → 0.4.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/README.md +153 -135
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +276 -62
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +19 -2
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +105 -0
- package/ios/HybridLiteRTLM.swift +1344 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +113 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +65 -0
- package/lib/__mocks__/react-native-nitro-modules.js +60 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +124 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +68 -0
- package/lib/hooks.js +27 -3
- package/lib/index.d.ts +6 -2
- package/lib/index.js +8 -8
- package/lib/modelFactory.js +82 -63
- package/lib/specs/LiteRTLM.nitro.d.ts +87 -2
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +2 -2
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +94 -9
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +5 -1
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +40 -3
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +74 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +28 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +46 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +57 -1
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +414 -3
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +41 -3
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +4 -6
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +10 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +240 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +71 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +431 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +203 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +83 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +9 -2
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +22 -2
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +99 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/package.json +22 -11
- package/react-native-litert-lm.podspec +17 -19
- package/scripts/download-ios-frameworks.sh +17 -50
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +40 -18
- package/src/__mocks__/react-native-nitro-modules.ts +58 -0
- package/src/__tests__/hooks.test.ts +153 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +96 -0
- package/src/hooks.ts +29 -7
- package/src/index.ts +7 -10
- package/src/modelFactory.ts +104 -80
- package/src/specs/LiteRTLM.nitro.ts +106 -2
- package/cpp/HybridLiteRTLM.cpp +0 -939
- package/cpp/HybridLiteRTLM.hpp +0 -169
- package/cpp/IOSDownloadHelper.h +0 -24
- package/ios/IOSDownloadHelper.mm +0 -129
- package/scripts/build-ios-engine.sh +0 -302
- package/scripts/stubs/cxx_bridge_stubs.cc +0 -224
- package/scripts/stubs/gemma_model_constraint_provider.cc +0 -46
- package/scripts/stubs/llguidance_stubs.c +0 -101
- package/src/templates.ts +0 -105
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-litert-lm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"litertLm": {
|
|
5
|
-
"version": "0.
|
|
6
|
-
"androidMavenVersion": "0.
|
|
7
|
-
"iosGitTag": "v0.
|
|
5
|
+
"version": "0.12.0",
|
|
6
|
+
"androidMavenVersion": "0.12.0",
|
|
7
|
+
"iosGitTag": "v0.12.0"
|
|
8
8
|
},
|
|
9
9
|
"description": "High-performance LLM inference for React Native using LiteRT-LM. Optimized for Gemma 4 and other on-device language models.",
|
|
10
10
|
"license": "MIT",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"android/src",
|
|
44
44
|
"android/build.gradle",
|
|
45
45
|
"android/CMakeLists.txt",
|
|
46
|
-
"ios
|
|
47
|
-
"ios
|
|
46
|
+
"ios/**/*.swift",
|
|
47
|
+
"ios/**/*.mm",
|
|
48
|
+
"ios/**/*.m",
|
|
48
49
|
"cpp",
|
|
49
50
|
"nitrogen/generated",
|
|
50
51
|
"scripts/postinstall.js",
|
|
52
|
+
"scripts/framework-source.js",
|
|
51
53
|
"scripts/download-ios-frameworks.sh",
|
|
52
|
-
"scripts/build-ios-engine.sh",
|
|
53
|
-
"scripts/stubs",
|
|
54
54
|
"react-native.config.js",
|
|
55
55
|
"react-native-litert-lm.podspec",
|
|
56
56
|
"app.plugin.js",
|
|
@@ -64,11 +64,14 @@
|
|
|
64
64
|
"postinstall": "node scripts/postinstall.js",
|
|
65
65
|
"build": "tsc",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "jest",
|
|
67
68
|
"lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
|
|
68
|
-
"prepack": "npm run build",
|
|
69
|
+
"prepack": "npx nitrogen && npm run build",
|
|
69
70
|
"specs": "npx nitrogen",
|
|
70
71
|
"clean": "rm -rf lib android/build ios/build ios/Frameworks nitrogen/generated",
|
|
71
72
|
"download-frameworks": "scripts/download-ios-frameworks.sh",
|
|
73
|
+
"check-framework-release": "node scripts/check-framework-release.js",
|
|
74
|
+
"prepublishOnly": "node scripts/check-framework-release.js",
|
|
72
75
|
"android": "expo run:android",
|
|
73
76
|
"android:clean": "cd android && ./gradlew clean",
|
|
74
77
|
"ios": "expo run:ios",
|
|
@@ -76,16 +79,24 @@
|
|
|
76
79
|
},
|
|
77
80
|
"devDependencies": {
|
|
78
81
|
"@expo/config-plugins": "~55.0.0",
|
|
82
|
+
"@types/jest": "^30.0.0",
|
|
79
83
|
"@types/react": "~19.2.10",
|
|
80
|
-
"react-
|
|
84
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
85
|
+
"jest": "^30.4.2",
|
|
86
|
+
"react": "^19.2.6",
|
|
87
|
+
"react-native": "^0.85.3",
|
|
88
|
+
"react-native-nitro-modules": "^0.35.9",
|
|
89
|
+
"react-test-renderer": "^19.2.6",
|
|
81
90
|
"release-it": "^19.2.4",
|
|
91
|
+
"ts-jest": "^29.4.10",
|
|
92
|
+
"ts-node": "^10.9.2",
|
|
82
93
|
"typescript": "^5.0.0"
|
|
83
94
|
},
|
|
84
95
|
"peerDependencies": {
|
|
85
96
|
"expo": ">=55.0.0",
|
|
86
97
|
"react": "*",
|
|
87
98
|
"react-native": "*",
|
|
88
|
-
"react-native-nitro-modules": "^0.35.
|
|
99
|
+
"react-native-nitro-modules": "^0.35.9"
|
|
89
100
|
},
|
|
90
101
|
"peerDependenciesMeta": {
|
|
91
102
|
"expo": {
|
|
@@ -9,36 +9,30 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.homepage = package["homepage"]
|
|
10
10
|
s.license = package["license"]
|
|
11
11
|
s.authors = package["author"]
|
|
12
|
-
s.platforms = { :ios => "15.
|
|
12
|
+
s.platforms = { :ios => "15.1" }
|
|
13
|
+
s.module_name = "LiteRTLM"
|
|
13
14
|
s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
|
|
14
15
|
|
|
15
|
-
s.swift_version = '5.
|
|
16
|
+
s.swift_version = '5.9'
|
|
16
17
|
|
|
17
18
|
s.source_files = [
|
|
18
|
-
#
|
|
19
|
-
"
|
|
20
|
-
#
|
|
21
|
-
"ios/**/*.{m,mm}",
|
|
22
|
-
# Nitrogen generated iOS bridge
|
|
19
|
+
# Swift, Objective-C/C++ implementation & autolinking
|
|
20
|
+
"ios/*.{swift,m,mm}",
|
|
21
|
+
# Nitrogen generated iOS bridge & shared C++ interfaces
|
|
23
22
|
"nitrogen/generated/ios/**/*.{mm,swift}",
|
|
23
|
+
"nitrogen/generated/shared/c++/**/*.{hpp,cpp}",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Prebuilt LiteRT-LM C engine (static library built from Bazel //c:engine target).
|
|
32
|
-
# Downloaded from GitHub releases by postinstall.js, or built locally via:
|
|
33
|
-
# scripts/build-ios-engine.sh
|
|
34
|
-
s.vendored_frameworks = 'ios/Frameworks/LiteRTLM.xcframework'
|
|
26
|
+
# Prebuilt LiteRT-LM C engine (xcframework). Not shipped in the npm tarball;
|
|
27
|
+
# fetched on install by scripts/postinstall.js (asset/tag defined in
|
|
28
|
+
# scripts/framework-source.js). Manual/upstream fallback:
|
|
29
|
+
# scripts/download-ios-frameworks.sh.
|
|
30
|
+
s.vendored_frameworks = 'ios/Frameworks/CLiteRTLM.xcframework'
|
|
35
31
|
|
|
36
32
|
s.pod_target_xcconfig = {
|
|
37
33
|
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20',
|
|
38
|
-
'
|
|
34
|
+
'SWIFT_VERSION' => '5.9',
|
|
39
35
|
'HEADER_SEARCH_PATHS' => [
|
|
40
|
-
'"$(PODS_TARGET_SRCROOT)/cpp"',
|
|
41
|
-
'"$(PODS_TARGET_SRCROOT)/cpp/include"',
|
|
42
36
|
'"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++"',
|
|
43
37
|
'"$(PODS_TARGET_SRCROOT)/nitrogen/generated/ios"',
|
|
44
38
|
].join(' '),
|
|
@@ -59,6 +53,10 @@ Pod::Spec.new do |s|
|
|
|
59
53
|
s.frameworks = ['Metal', 'MetalPerformanceShaders', 'Accelerate', 'CoreML', 'CoreGraphics']
|
|
60
54
|
s.libraries = ['c++']
|
|
61
55
|
|
|
56
|
+
s.test_spec 'Tests' do |test_spec|
|
|
57
|
+
test_spec.source_files = 'ios/Tests/**/*.{swift}'
|
|
58
|
+
end
|
|
59
|
+
|
|
62
60
|
install_modules_dependencies(s)
|
|
63
61
|
end
|
|
64
62
|
|
|
@@ -1,72 +1,39 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# download-ios-frameworks.sh
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# releases. If the prebuilt asset is not available, falls back to building
|
|
6
|
-
# from source via Bazel (see build-ios-engine.sh).
|
|
7
|
-
#
|
|
8
|
-
# The XCFramework contains a static library compiled from the LiteRT-LM
|
|
9
|
-
# C engine (//c:engine Bazel target) for both device (arm64) and simulator
|
|
10
|
-
# (sim_arm64).
|
|
11
|
-
#
|
|
12
|
-
# Usage:
|
|
13
|
-
# ./scripts/download-ios-frameworks.sh
|
|
3
|
+
# Downloads the official prebuilt LiteRT-LM iOS framework (CLiteRTLM.xcframework)
|
|
4
|
+
# directly from the google-ai-edge/LiteRT-LM releases.
|
|
14
5
|
|
|
15
6
|
set -euo pipefail
|
|
16
7
|
|
|
17
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
9
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
19
|
-
OUTPUT_DIR="$PROJECT_ROOT/ios/Frameworks"
|
|
20
|
-
C_API_HEADER_DIR="$PROJECT_ROOT/cpp/include"
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
PACKAGE_VERSION=$(node -e "console.log(require('$PROJECT_ROOT/package.json').version)" 2>/dev/null || echo "0.0.0")
|
|
27
|
-
GITHUB_REPO="hung-yueh/react-native-litert-lm"
|
|
28
|
-
ASSET_NAME="LiteRTLM-ios-frameworks.zip"
|
|
11
|
+
# Resolve the asset URL + output dir from the shared single source of truth
|
|
12
|
+
# (scripts/framework-source.js) so this manual path can't drift from postinstall.
|
|
13
|
+
RELEASE_URL="$(node -e "console.log(require('$SCRIPT_DIR/framework-source').ASSET_URL)")"
|
|
14
|
+
OUTPUT_DIR="$(node -e "console.log(require('$SCRIPT_DIR/framework-source').FRAMEWORKS_DIR)")"
|
|
29
15
|
|
|
30
16
|
# Skip if already present
|
|
31
|
-
if [ -d "$OUTPUT_DIR
|
|
32
|
-
echo "[LiteRT-LM] iOS
|
|
17
|
+
if [ -d "$OUTPUT_DIR/CLiteRTLM.xcframework" ]; then
|
|
18
|
+
echo "[LiteRT-LM] iOS CLiteRTLM.xcframework already present, skipping download."
|
|
33
19
|
exit 0
|
|
34
20
|
fi
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
echo "[LiteRT-LM] Vendoring C API header..."
|
|
38
|
-
mkdir -p "$C_API_HEADER_DIR"
|
|
39
|
-
curl -fsSL -o "$C_API_HEADER_DIR/litert_lm_engine.h" \
|
|
40
|
-
"${GITHUB_RAW}/c/engine.h" 2>/dev/null || true
|
|
41
|
-
|
|
42
|
-
# ---- Try downloading prebuilt from our GitHub releases --------------------
|
|
43
|
-
RELEASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${ASSET_NAME}"
|
|
44
|
-
|
|
45
|
-
echo "[LiteRT-LM] Attempting to download prebuilt iOS engine from:"
|
|
22
|
+
echo "[LiteRT-LM] Downloading prebuilt iOS engine from Google's release:"
|
|
46
23
|
echo " ${RELEASE_URL}"
|
|
47
24
|
|
|
25
|
+
mkdir -p "$OUTPUT_DIR"
|
|
48
26
|
TMP_ZIP="$PROJECT_ROOT/.ios-frameworks-tmp.zip"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
|
|
28
|
+
if curl -fsSL -o "$TMP_ZIP" "$RELEASE_URL"; then
|
|
29
|
+
echo "[LiteRT-LM] Download successful, extracting to $OUTPUT_DIR..."
|
|
30
|
+
rm -rf "$OUTPUT_DIR/CLiteRTLM.xcframework"
|
|
53
31
|
unzip -o -q "$TMP_ZIP" -d "$OUTPUT_DIR"
|
|
54
32
|
rm -f "$TMP_ZIP"
|
|
55
|
-
echo "[LiteRT-LM] ✅ iOS frameworks installed
|
|
33
|
+
echo "[LiteRT-LM] ✅ iOS frameworks successfully installed."
|
|
56
34
|
exit 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
rm -f "$TMP_ZIP"
|
|
60
|
-
echo "[LiteRT-LM] Prebuilt not available for v${PACKAGE_VERSION}."
|
|
61
|
-
|
|
62
|
-
# ---- Fall back to building from source ------------------------------------
|
|
63
|
-
echo "[LiteRT-LM] Falling back to building from source via Bazel..."
|
|
64
|
-
echo ""
|
|
65
|
-
|
|
66
|
-
if [ -x "$SCRIPT_DIR/build-ios-engine.sh" ]; then
|
|
67
|
-
exec "$SCRIPT_DIR/build-ios-engine.sh"
|
|
68
35
|
else
|
|
69
|
-
|
|
70
|
-
echo "
|
|
36
|
+
rm -f "$TMP_ZIP"
|
|
37
|
+
echo "Error: Failed to download LiteRT-LM iOS framework from ${RELEASE_URL}"
|
|
71
38
|
exit 1
|
|
72
39
|
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* framework-source.js
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the prebuilt iOS framework artifact, shared by
|
|
5
|
+
* scripts/postinstall.js, scripts/download-ios-frameworks.sh, and
|
|
6
|
+
* scripts/check-framework-release.js, so the asset name, tag, and URL can never
|
|
7
|
+
* drift between the install path and the release-time guardrail.
|
|
8
|
+
*
|
|
9
|
+
* The artifact is Google's canonical LiteRT-LM release (NOT a per-version
|
|
10
|
+
* re-host of this package), pinned to the LiteRT-LM **engine** version
|
|
11
|
+
* (`litertLm.iosGitTag` in package.json), NOT this wrapper's npm version. So:
|
|
12
|
+
* - patch releases of this wrapper reuse the same framework (no re-upload),
|
|
13
|
+
* - there is no per-release asset for the maintainer to forget to upload,
|
|
14
|
+
* - the guardrail checks the exact URL consumers fetch.
|
|
15
|
+
*
|
|
16
|
+
* Override the host/asset (e.g. to point at a private mirror) via env vars:
|
|
17
|
+
* LITERT_FRAMEWORK_REPO (default: google-ai-edge/LiteRT-LM)
|
|
18
|
+
* LITERT_FRAMEWORK_ASSET (default: CLiteRTLM.xcframework.zip)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const packageJson = require('../package.json');
|
|
23
|
+
|
|
24
|
+
/** GitHub repo that hosts the framework release asset. */
|
|
25
|
+
const GITHUB_REPO = process.env.LITERT_FRAMEWORK_REPO || 'google-ai-edge/LiteRT-LM';
|
|
26
|
+
|
|
27
|
+
/** Release asset filename. Must match the file on the GitHub release. */
|
|
28
|
+
const ASSET_NAME = process.env.LITERT_FRAMEWORK_ASSET || 'CLiteRTLM.xcframework.zip';
|
|
29
|
+
|
|
30
|
+
/** Release tag the asset lives under — the LiteRT-LM engine git tag, e.g. "v0.12.0". */
|
|
31
|
+
const FRAMEWORK_TAG = packageJson.litertLm.iosGitTag;
|
|
32
|
+
|
|
33
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
const FRAMEWORKS_DIR = path.join(PACKAGE_ROOT, 'ios', 'Frameworks');
|
|
35
|
+
|
|
36
|
+
/** Fully-resolved download URL for the framework zip. */
|
|
37
|
+
const ASSET_URL = `https://github.com/${GITHUB_REPO}/releases/download/${FRAMEWORK_TAG}/${ASSET_NAME}`;
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
GITHUB_REPO,
|
|
41
|
+
ASSET_NAME,
|
|
42
|
+
FRAMEWORK_TAG,
|
|
43
|
+
PACKAGE_ROOT,
|
|
44
|
+
FRAMEWORKS_DIR,
|
|
45
|
+
ASSET_URL,
|
|
46
|
+
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* Downloads prebuilt LiteRT-LM iOS frameworks from this package's GitHub
|
|
6
6
|
* releases when consumers run `npm install react-native-litert-lm`.
|
|
7
7
|
*
|
|
8
|
+
* The framework is intentionally NOT shipped inside the npm tarball (it is
|
|
9
|
+
* ~40MB and irrelevant to Android-only consumers). The asset name, tag, and
|
|
10
|
+
* URL come from scripts/framework-source.js so they stay in lockstep with the
|
|
11
|
+
* release-time guardrail (scripts/check-framework-release.js).
|
|
12
|
+
*
|
|
8
13
|
* Skips download if:
|
|
9
14
|
* - Not on macOS (iOS builds require macOS)
|
|
10
15
|
* - Frameworks already exist
|
|
11
|
-
* -
|
|
16
|
+
* - SKIP_IOS_FRAMEWORK_DOWNLOAD=1 (e.g. Android-only / CI builds)
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
const { execSync } = require('child_process');
|
|
@@ -16,14 +21,12 @@ const fs = require('fs');
|
|
|
16
21
|
const path = require('path');
|
|
17
22
|
const https = require('https');
|
|
18
23
|
|
|
19
|
-
const
|
|
20
|
-
const PACKAGE_VERSION = PACKAGE_JSON.version;
|
|
21
|
-
const GITHUB_REPO = 'hung-yueh/react-native-litert-lm';
|
|
22
|
-
const ASSET_NAME = 'LiteRTLM-ios-frameworks.zip';
|
|
24
|
+
const { ASSET_URL, FRAMEWORKS_DIR, FRAMEWORK_TAG } = require('./framework-source');
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
/** ZIP local-file-header magic ("PK\x03\x04"). Guards against truncated downloads / HTML error pages. */
|
|
27
|
+
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
28
|
+
/** A valid framework zip is well over this; anything smaller is certainly an error page. */
|
|
29
|
+
const MIN_VALID_BYTES = 1024 * 1024; // 1 MB
|
|
27
30
|
|
|
28
31
|
function log(msg) {
|
|
29
32
|
console.log(`[react-native-litert-lm] ${msg}`);
|
|
@@ -74,25 +77,46 @@ function downloadFile(url, destPath, maxRedirects = 5) {
|
|
|
74
77
|
const file = fs.createWriteStream(destPath);
|
|
75
78
|
res.pipe(file);
|
|
76
79
|
file.on('finish', () => {
|
|
77
|
-
file.close();
|
|
78
|
-
resolve();
|
|
80
|
+
file.close(resolve);
|
|
79
81
|
});
|
|
80
82
|
file.on('error', reject);
|
|
81
83
|
}).on('error', reject);
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Verify the downloaded file is a non-trivial ZIP before we trust it.
|
|
89
|
+
* Catches GitHub HTML error pages and truncated downloads that would
|
|
90
|
+
* otherwise fail cryptically at `unzip` or, worse, link a corrupt framework.
|
|
91
|
+
*/
|
|
92
|
+
function assertValidZip(zipPath) {
|
|
93
|
+
const { size } = fs.statSync(zipPath);
|
|
94
|
+
if (size < MIN_VALID_BYTES) {
|
|
95
|
+
throw new Error(`downloaded asset is only ${size} bytes — expected a multi-MB framework zip (likely an error page or truncated download)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const header = Buffer.alloc(4);
|
|
99
|
+
const fd = fs.openSync(zipPath, 'r');
|
|
100
|
+
try {
|
|
101
|
+
fs.readSync(fd, header, 0, 4, 0);
|
|
102
|
+
} finally {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
if (!header.equals(ZIP_MAGIC)) {
|
|
106
|
+
throw new Error('downloaded asset is not a valid ZIP (bad magic bytes)');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
async function main() {
|
|
86
111
|
if (shouldSkip()) return;
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
log(`Downloading iOS frameworks from: ${releaseUrl}`);
|
|
113
|
+
log(`Downloading iOS frameworks (engine ${FRAMEWORK_TAG}) from: ${ASSET_URL}`);
|
|
91
114
|
|
|
92
|
-
const tmpZip = path.join(
|
|
115
|
+
const tmpZip = path.join(path.dirname(FRAMEWORKS_DIR), '.ios-frameworks-tmp.zip');
|
|
93
116
|
|
|
94
117
|
try {
|
|
95
|
-
await downloadFile(
|
|
118
|
+
await downloadFile(ASSET_URL, tmpZip);
|
|
119
|
+
assertValidZip(tmpZip);
|
|
96
120
|
|
|
97
121
|
// Extract
|
|
98
122
|
fs.mkdirSync(FRAMEWORKS_DIR, { recursive: true });
|
|
@@ -108,11 +132,9 @@ async function main() {
|
|
|
108
132
|
|
|
109
133
|
log(`Error: Could not download iOS frameworks: ${err.message}`);
|
|
110
134
|
log('iOS builds will not work until frameworks are available.');
|
|
111
|
-
log('Run: ./scripts/download-ios-frameworks.sh to download manually
|
|
112
|
-
log(' or: ./scripts/build-ios-engine.sh to build from source.');
|
|
135
|
+
log('Run: ./scripts/download-ios-frameworks.sh to download manually.');
|
|
113
136
|
|
|
114
137
|
// Fail fast on macOS so users discover the problem now, not at Xcode link time.
|
|
115
|
-
// Skip SKIP_IOS_FRAMEWORK_DOWNLOAD is already checked above.
|
|
116
138
|
if (process.platform === 'darwin') {
|
|
117
139
|
log('Set SKIP_IOS_FRAMEWORK_DOWNLOAD=1 to suppress this error (e.g. Android-only builds).');
|
|
118
140
|
process.exit(1);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const mockLiteRTLM = {
|
|
2
|
+
isReady: jest.fn(() => false),
|
|
3
|
+
loadModel: jest.fn().mockResolvedValue(undefined),
|
|
4
|
+
sendMessage: jest.fn().mockResolvedValue("Mock response"),
|
|
5
|
+
sendMessageWithImage: jest.fn().mockResolvedValue("Mock vision response"),
|
|
6
|
+
downloadModel: jest.fn(async (url, fileName, onProgress) => {
|
|
7
|
+
onProgress?.(1.0);
|
|
8
|
+
return "/mock/path/model.litertlm";
|
|
9
|
+
}),
|
|
10
|
+
deleteModel: jest.fn().mockResolvedValue(undefined),
|
|
11
|
+
sendMessageWithAudio: jest.fn().mockResolvedValue("Mock audio response"),
|
|
12
|
+
sendMultimodalMessage: jest.fn().mockResolvedValue("Mock multimodal response"),
|
|
13
|
+
sendMessageAsync: jest.fn((msg, onToken) => {
|
|
14
|
+
onToken("Mock ", false);
|
|
15
|
+
onToken("token", true);
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}),
|
|
18
|
+
sendMessageWithImageAsync: jest.fn((msg, imagePath, onToken) => {
|
|
19
|
+
onToken("Mock vision ", false);
|
|
20
|
+
onToken("token", true);
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}),
|
|
23
|
+
sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => {
|
|
24
|
+
onToken("Mock audio ", false);
|
|
25
|
+
onToken("token", true);
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
}),
|
|
28
|
+
getHistory: jest.fn(() => []),
|
|
29
|
+
resetConversation: jest.fn(),
|
|
30
|
+
getStats: jest.fn(() => ({
|
|
31
|
+
promptTokens: 10,
|
|
32
|
+
completionTokens: 20,
|
|
33
|
+
totalTokens: 30,
|
|
34
|
+
timeToFirstToken: 5,
|
|
35
|
+
totalTime: 50,
|
|
36
|
+
tokensPerSecond: 400,
|
|
37
|
+
})),
|
|
38
|
+
countTokens: jest.fn(() => -1),
|
|
39
|
+
getMemoryUsage: jest.fn(() => ({
|
|
40
|
+
nativeHeapBytes: 1000000,
|
|
41
|
+
residentBytes: 2000000,
|
|
42
|
+
availableMemoryBytes: 4000000,
|
|
43
|
+
isLowMemory: false,
|
|
44
|
+
})),
|
|
45
|
+
close: jest.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const NitroModules = {
|
|
49
|
+
createHybridObject: jest.fn((name: string) => {
|
|
50
|
+
if (name === "LiteRTLM") {
|
|
51
|
+
return mockLiteRTLM;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Mock not implemented for hybrid object: ${name}`);
|
|
54
|
+
}),
|
|
55
|
+
createNativeArrayBuffer: jest.fn((size: number) => {
|
|
56
|
+
return new ArrayBuffer(size);
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Configure React act environment
|
|
2
|
+
(global as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
3
|
+
|
|
4
|
+
import { useModel } from '../hooks';
|
|
5
|
+
import { mockLiteRTLM } from '../__mocks__/react-native-nitro-modules';
|
|
6
|
+
import TestRenderer from 'react-test-renderer';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
// Mock react-native
|
|
10
|
+
jest.mock('react-native', () => ({
|
|
11
|
+
Platform: {
|
|
12
|
+
OS: 'ios',
|
|
13
|
+
select: jest.fn((dict) => dict.ios),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Helper to render and test hooks using react-test-renderer
|
|
18
|
+
function renderHook<P, R>(callback: (props: P) => R, initialProps?: P) {
|
|
19
|
+
let result = { current: null as unknown as R };
|
|
20
|
+
|
|
21
|
+
const TestComponent = ({ props }: { props: P }) => {
|
|
22
|
+
result.current = callback(props);
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let renderer: TestRenderer.ReactTestRenderer;
|
|
27
|
+
TestRenderer.act(() => {
|
|
28
|
+
renderer = TestRenderer.create(React.createElement(TestComponent, { props: initialProps as P }));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const rerender = (newProps: P) => {
|
|
32
|
+
TestRenderer.act(() => {
|
|
33
|
+
renderer.update(React.createElement(TestComponent, { props: newProps }));
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const unmount = () => {
|
|
38
|
+
TestRenderer.act(() => {
|
|
39
|
+
renderer.unmount();
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { result, rerender, unmount };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('useModel React Hook Unit Tests', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should initialize with correct default state and call loadModel automatically when autoLoad is true', async () => {
|
|
52
|
+
let hookResult: any;
|
|
53
|
+
|
|
54
|
+
await TestRenderer.act(async () => {
|
|
55
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(hookResult.result.current.isReady).toBe(true);
|
|
59
|
+
expect(hookResult.result.current.isGenerating).toBe(false);
|
|
60
|
+
expect(hookResult.result.current.downloadProgress).toBe(1); // loadModel completed
|
|
61
|
+
expect(hookResult.result.current.error).toBeNull();
|
|
62
|
+
expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not call loadModel automatically when autoLoad is false', async () => {
|
|
66
|
+
let hookResult: any;
|
|
67
|
+
|
|
68
|
+
await TestRenderer.act(async () => {
|
|
69
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
73
|
+
expect(mockLiteRTLM.loadModel).not.toHaveBeenCalled();
|
|
74
|
+
|
|
75
|
+
// Call load manually
|
|
76
|
+
await TestRenderer.act(async () => {
|
|
77
|
+
await hookResult.result.current.load();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(hookResult.result.current.isReady).toBe(true);
|
|
81
|
+
expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle model load failure gracefully', async () => {
|
|
85
|
+
mockLiteRTLM.loadModel.mockRejectedValueOnce(new Error("Model load failed"));
|
|
86
|
+
let hookResult: any;
|
|
87
|
+
|
|
88
|
+
await TestRenderer.act(async () => {
|
|
89
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
93
|
+
expect(hookResult.result.current.error).toBe("Model load failed");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should generate text successfully and trigger memory summary update', async () => {
|
|
97
|
+
let hookResult: any;
|
|
98
|
+
|
|
99
|
+
await TestRenderer.act(async () => {
|
|
100
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
|
|
101
|
+
autoLoad: true,
|
|
102
|
+
enableMemoryTracking: true
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let response = "";
|
|
107
|
+
await TestRenderer.act(async () => {
|
|
108
|
+
response = await hookResult.result.current.generate("Test prompt");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(response).toBe("Mock token");
|
|
112
|
+
expect(mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
|
|
113
|
+
expect(hookResult.result.current.memorySummary).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should reset conversation correctly', async () => {
|
|
117
|
+
let hookResult: any;
|
|
118
|
+
|
|
119
|
+
await TestRenderer.act(async () => {
|
|
120
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
hookResult.result.current.reset();
|
|
124
|
+
expect(mockLiteRTLM.resetConversation).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should delete model file correctly', async () => {
|
|
128
|
+
let hookResult: any;
|
|
129
|
+
|
|
130
|
+
await TestRenderer.act(async () => {
|
|
131
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await TestRenderer.act(async () => {
|
|
135
|
+
await hookResult.result.current.deleteModel();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(mockLiteRTLM.deleteModel).toHaveBeenCalledWith('model.litertlm');
|
|
139
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
140
|
+
expect(hookResult.result.current.downloadProgress).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should call close on unmount', async () => {
|
|
144
|
+
let hookResult: any;
|
|
145
|
+
|
|
146
|
+
await TestRenderer.act(async () => {
|
|
147
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
hookResult.unmount();
|
|
151
|
+
expect(mockLiteRTLM.close).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|