catalyst-core-internal 0.1.4 → 0.1.6
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/bin/catalyst.js +1 -8
- package/changelog.md +21 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +1 -12
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +3 -18
- package/dist/native/buildAppAndroid.js +2 -2
- package/dist/native/buildAppIos.js +17 -10
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +2 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +0 -6
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +0 -4
- package/mcp_v2/conversion-tasks.json +326 -0
- package/mcp_v2/knowledge-base.json +1068 -0
- package/mcp_v2/lib/helpers.js +170 -0
- package/mcp_v2/mcp.js +563 -0
- package/mcp_v2/package.json +13 -0
- package/mcp_v2/schema.sql +88 -0
- package/mcp_v2/setup.js +282 -0
- package/mcp_v2/tools/build.js +686 -0
- package/mcp_v2/tools/config.js +453 -0
- package/mcp_v2/tools/conversion.js +799 -0
- package/mcp_v2/tools/debug.js +113 -0
- package/mcp_v2/tools/knowledge.js +219 -0
- package/mcp_v2/tools/sync.js +23 -0
- package/mcp_v2/tools/tasks.js +945 -0
- package/package.json +7 -15
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +0 -7
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +0 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +0 -253
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +0 -139
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +0 -43
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +0 -28
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +0 -19
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +0 -5
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +0 -6
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +0 -364
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +0 -14
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +0 -160
- package/dist/native/plugin-bridge/PluginBridge.js +0 -1
- package/dist/native/pluginComposerAndroid.js +0 -9
- package/dist/native/pluginComposerIos.js +0 -7
- package/dist/scripts/plugins.js +0 -1
- package/license +0 -10
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catalyst-core-internal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"main": "index.js",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
5
|
"description": "Web framework that provides great performance out of the box",
|
|
6
6
|
"bin": {
|
|
7
7
|
"catalyst": "bin/catalyst.js"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/tata1mg/catalyst-core.git"
|
|
11
|
+
"url": "https://github.com/tata1mg/catalyst-core.git",
|
|
12
|
+
"directory": "packages/catalyst-core"
|
|
12
13
|
},
|
|
13
14
|
"_moduleAliases": {
|
|
14
15
|
"@catalyst/template": ".",
|
|
@@ -31,17 +32,16 @@
|
|
|
31
32
|
"./caching": "./dist/caching.js",
|
|
32
33
|
"./router/ClientRouter": "./dist/router/ClientRouter.js",
|
|
33
34
|
"./WebBridge": "./dist/native/bridge/WebBridge.js",
|
|
34
|
-
"./PluginBridge": "./dist/native/plugin-bridge/PluginBridge.js",
|
|
35
35
|
"./hooks": "./dist/native/bridge/hooks.js",
|
|
36
36
|
"./sentry": "./dist/sentry.js",
|
|
37
37
|
"./otel": "./dist/otel.js"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"lint": "eslint .",
|
|
41
|
-
"lint-staged": "lint-staged",
|
|
42
41
|
"prettify": "prettier . --write",
|
|
43
|
-
"prepare": "babel src --out-dir ./dist --ignore '**/.build/**' && mkdir -p dist/native && cp -r src/native/androidProject src/native/build.swift src/native/assets
|
|
44
|
-
"prepublishOnly": "npm
|
|
42
|
+
"prepare": "babel src --out-dir ./dist --ignore '**/.build/**' && mkdir -p dist/native && cp -r src/native/androidProject src/native/build.swift src/native/assets dist/native/ && mkdir -p dist/native/iosnativeWebView && tar -C src/native/iosnativeWebView --exclude='.build' --exclude='.package-config-hash' -cf - . | tar -C dist/native/iosnativeWebView -xf -",
|
|
43
|
+
"prepublishOnly": "npm run prepare",
|
|
44
|
+
"test:fixture": "sh ../../scripts/test-catalyst-core.sh"
|
|
45
45
|
},
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"dependencies": {
|
|
@@ -114,14 +114,6 @@
|
|
|
114
114
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
115
115
|
"eslint-plugin-risxss": "^2.1.0",
|
|
116
116
|
"eslint-plugin-security": "^3.0.0",
|
|
117
|
-
"husky": "^9.0.11",
|
|
118
|
-
"lint-staged": "^15.2.2",
|
|
119
117
|
"prettier": "^3.2.5"
|
|
120
|
-
},
|
|
121
|
-
"lint-staged": {
|
|
122
|
-
"*.{js,jsx}": [
|
|
123
|
-
"eslint .",
|
|
124
|
-
"prettier . --write"
|
|
125
|
-
]
|
|
126
118
|
}
|
|
127
119
|
}
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
package io.yourname.androidproject.plugins
|
|
2
|
-
|
|
3
|
-
import android.app.Activity
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.util.Log
|
|
6
|
-
import android.webkit.JavascriptInterface
|
|
7
|
-
import android.webkit.WebView
|
|
8
|
-
import io.yourname.androidproject.CatalystConstants
|
|
9
|
-
import org.json.JSONArray
|
|
10
|
-
import org.json.JSONException
|
|
11
|
-
import org.json.JSONObject
|
|
12
|
-
import java.util.Properties
|
|
13
|
-
|
|
14
|
-
internal data class PluginRequest(
|
|
15
|
-
val pluginId: String,
|
|
16
|
-
val command: String,
|
|
17
|
-
val data: JSONObject?,
|
|
18
|
-
val requestId: String?
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
private class PluginBridgeRuntimeError(
|
|
22
|
-
val publicMessage: String,
|
|
23
|
-
val code: String,
|
|
24
|
-
cause: Throwable? = null
|
|
25
|
-
) : Exception(publicMessage, cause)
|
|
26
|
-
|
|
27
|
-
class PluginBridge(
|
|
28
|
-
private val activity: Activity,
|
|
29
|
-
private val webView: WebView,
|
|
30
|
-
private val properties: Properties
|
|
31
|
-
) {
|
|
32
|
-
companion object {
|
|
33
|
-
private const val TAG = "PluginBridge"
|
|
34
|
-
private const val ERROR_EVENT = "PLUGIN_BRIDGE_ERROR"
|
|
35
|
-
private const val SYSTEM_PLUGIN_ID = "__bridge__"
|
|
36
|
-
private const val ERROR_CODE_INVALID_PAYLOAD = "INVALID_PAYLOAD"
|
|
37
|
-
private const val ERROR_CODE_PLUGIN_NOT_FOUND = "PLUGIN_NOT_FOUND"
|
|
38
|
-
private const val ERROR_CODE_COMMAND_NOT_SUPPORTED = "COMMAND_NOT_SUPPORTED"
|
|
39
|
-
private const val ERROR_CODE_PLUGIN_NOT_REGISTERED = "PLUGIN_NOT_REGISTERED"
|
|
40
|
-
private const val ERROR_CODE_PLUGIN_INSTANTIATION_FAILED = "PLUGIN_INSTANTIATION_FAILED"
|
|
41
|
-
private const val ERROR_CODE_PLUGIN_EXECUTION_FAILED = "PLUGIN_EXECUTION_FAILED"
|
|
42
|
-
|
|
43
|
-
private fun readRequiredString(body: JSONObject, key: String): String {
|
|
44
|
-
if (!body.has(key) || body.isNull(key)) {
|
|
45
|
-
return ""
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
val rawValue = body.get(key)
|
|
49
|
-
if (rawValue !is String) {
|
|
50
|
-
throw IllegalArgumentException("$key must be a string")
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return rawValue.trim()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private fun readOptionalString(body: JSONObject, key: String): String? {
|
|
57
|
-
if (!body.has(key) || body.isNull(key)) {
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
val rawValue = body.get(key)
|
|
62
|
-
if (rawValue !is String) {
|
|
63
|
-
throw IllegalArgumentException("$key must be a string when provided")
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return rawValue.trim().ifEmpty { null }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private fun readOptionalObject(body: JSONObject, key: String): JSONObject? {
|
|
70
|
-
if (!body.has(key) || body.isNull(key)) {
|
|
71
|
-
return null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
val rawValue = body.get(key)
|
|
75
|
-
if (rawValue !is JSONObject) {
|
|
76
|
-
throw IllegalArgumentException("$key must be an object when provided")
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return rawValue
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
internal fun parseRequest(payload: String?): PluginRequest {
|
|
83
|
-
if (payload.isNullOrBlank()) {
|
|
84
|
-
throw IllegalArgumentException("Payload is required")
|
|
85
|
-
}
|
|
86
|
-
val messageSize = payload.toByteArray(Charsets.UTF_8).size
|
|
87
|
-
if (messageSize > CatalystConstants.Bridge.MAX_MESSAGE_SIZE) {
|
|
88
|
-
throw IllegalArgumentException("Payload exceeds maximum size")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
val body = JSONObject(payload)
|
|
92
|
-
return PluginRequest(
|
|
93
|
-
pluginId = readRequiredString(body, "pluginId"),
|
|
94
|
-
command = readRequiredString(body, "command"),
|
|
95
|
-
data = readOptionalObject(body, "data"),
|
|
96
|
-
requestId = readOptionalString(body, "requestId")
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private val pluginIdToClassName = GeneratedPluginIndex.pluginIdToClassName
|
|
102
|
-
private val pluginToCommands = GeneratedPluginIndex.pluginToCommands
|
|
103
|
-
|
|
104
|
-
@JavascriptInterface
|
|
105
|
-
fun emit(payload: String?) {
|
|
106
|
-
var request: PluginRequest? = null
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
request = parseRequest(payload)
|
|
110
|
-
|
|
111
|
-
if (request.pluginId.isEmpty()) {
|
|
112
|
-
sendBridgeError("pluginId is required", ERROR_CODE_INVALID_PAYLOAD, request)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
if (request.command.isEmpty()) {
|
|
116
|
-
sendBridgeError("command is required", ERROR_CODE_INVALID_PAYLOAD, request)
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!hasPlugin(request.pluginId)) {
|
|
121
|
-
sendBridgeError("Unsupported plugin: ${request.pluginId}", ERROR_CODE_PLUGIN_NOT_FOUND, request)
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!hasCommand(request.pluginId, request.command)) {
|
|
126
|
-
sendBridgeError(
|
|
127
|
-
"Unsupported command '${request.command}' for plugin '${request.pluginId}'",
|
|
128
|
-
ERROR_CODE_COMMAND_NOT_SUPPORTED,
|
|
129
|
-
request
|
|
130
|
-
)
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
val plugin = try {
|
|
135
|
-
getPluginForId(request.pluginId)
|
|
136
|
-
} catch (error: PluginBridgeRuntimeError) {
|
|
137
|
-
sendBridgeError(error.publicMessage, error.code, request)
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
val callbackContext = PluginBridgeContext(
|
|
142
|
-
activity = activity,
|
|
143
|
-
webView = webView,
|
|
144
|
-
properties = properties,
|
|
145
|
-
pluginId = request.pluginId,
|
|
146
|
-
command = request.command,
|
|
147
|
-
requestId = request.requestId
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
plugin.handle(request.command, request.data, callbackContext)
|
|
151
|
-
} catch (error: IllegalArgumentException) {
|
|
152
|
-
sendBridgeError(error.message ?: "Invalid payload", ERROR_CODE_INVALID_PAYLOAD, request)
|
|
153
|
-
} catch (error: JSONException) {
|
|
154
|
-
sendBridgeError("Invalid JSON payload", ERROR_CODE_INVALID_PAYLOAD, request)
|
|
155
|
-
} catch (error: Exception) {
|
|
156
|
-
Log.e(TAG, "Plugin command failed for ${request?.pluginId ?: "<unknown>"}.${request?.command ?: "<unknown>"}", error)
|
|
157
|
-
sendBridgeError("Plugin execution failed", ERROR_CODE_PLUGIN_EXECUTION_FAILED, request)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private fun sendBridgeError(message: String, code: String, request: PluginRequest?) {
|
|
162
|
-
PluginBridgeContext(
|
|
163
|
-
activity = activity,
|
|
164
|
-
webView = webView,
|
|
165
|
-
properties = properties,
|
|
166
|
-
pluginId = SYSTEM_PLUGIN_ID,
|
|
167
|
-
command = request?.command,
|
|
168
|
-
requestId = request?.requestId
|
|
169
|
-
).callback(
|
|
170
|
-
ERROR_EVENT,
|
|
171
|
-
JSONObject().apply {
|
|
172
|
-
put("message", message)
|
|
173
|
-
put("code", code)
|
|
174
|
-
put("pluginId", request?.pluginId ?: SYSTEM_PLUGIN_ID)
|
|
175
|
-
request?.command?.takeIf { it.isNotEmpty() }?.let { put("command", it) }
|
|
176
|
-
}
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private fun hasPlugin(pluginId: String): Boolean {
|
|
181
|
-
return pluginIdToClassName.containsKey(pluginId)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private fun hasCommand(pluginId: String, command: String): Boolean {
|
|
185
|
-
return pluginToCommands[pluginId]?.contains(command) ?: false
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private fun getPluginForId(pluginId: String): CatalystPlugin {
|
|
189
|
-
val className = pluginIdToClassName[pluginId]
|
|
190
|
-
?: throw PluginBridgeRuntimeError(
|
|
191
|
-
"Plugin is not registered",
|
|
192
|
-
ERROR_CODE_PLUGIN_NOT_REGISTERED
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
return try {
|
|
196
|
-
val clazz = Class.forName(className)
|
|
197
|
-
val instance = clazz.getDeclaredConstructor().newInstance()
|
|
198
|
-
instance as? CatalystPlugin
|
|
199
|
-
?: throw IllegalStateException("Plugin class '$className' must implement CatalystPlugin")
|
|
200
|
-
} catch (error: Exception) {
|
|
201
|
-
Log.e(TAG, "Failed to instantiate plugin class $className for plugin $pluginId", error)
|
|
202
|
-
throw PluginBridgeRuntimeError(
|
|
203
|
-
"Plugin could not be initialized",
|
|
204
|
-
ERROR_CODE_PLUGIN_INSTANTIATION_FAILED,
|
|
205
|
-
error
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
class PluginBridgeContext(
|
|
212
|
-
val activity: Activity,
|
|
213
|
-
val webView: WebView,
|
|
214
|
-
val properties: Properties,
|
|
215
|
-
val pluginId: String,
|
|
216
|
-
val command: String?,
|
|
217
|
-
val requestId: String?
|
|
218
|
-
) {
|
|
219
|
-
val context: Context
|
|
220
|
-
get() = activity
|
|
221
|
-
|
|
222
|
-
fun callback(
|
|
223
|
-
eventName: String,
|
|
224
|
-
data: Any?,
|
|
225
|
-
command: String? = this.command
|
|
226
|
-
) {
|
|
227
|
-
require(eventName.isNotBlank()) { "Callback eventName is required" }
|
|
228
|
-
|
|
229
|
-
val pluginLiteral = JSONObject.quote(pluginId)
|
|
230
|
-
val eventLiteral = JSONObject.quote(eventName)
|
|
231
|
-
val dataLiteral = toJavaScriptLiteral(data)
|
|
232
|
-
val requestLiteral = requestId?.let(JSONObject::quote) ?: "null"
|
|
233
|
-
val commandLiteral = command?.takeIf { it.isNotBlank() }?.let(JSONObject::quote) ?: "null"
|
|
234
|
-
|
|
235
|
-
webView.post {
|
|
236
|
-
webView.evaluateJavascript(
|
|
237
|
-
"window.PluginBridgeWeb && window.PluginBridgeWeb.callback($pluginLiteral, $eventLiteral, $dataLiteral, $requestLiteral, $commandLiteral);",
|
|
238
|
-
null
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private fun toJavaScriptLiteral(value: Any?): String {
|
|
244
|
-
return when (value) {
|
|
245
|
-
null -> "null"
|
|
246
|
-
is JSONObject -> value.toString()
|
|
247
|
-
is JSONArray -> value.toString()
|
|
248
|
-
is Number, is Boolean -> value.toString()
|
|
249
|
-
is String -> JSONObject.quote(value)
|
|
250
|
-
else -> JSONObject.wrap(value)?.toString() ?: "null"
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
package io.yourname.androidproject.plugins
|
|
2
|
-
|
|
3
|
-
import io.yourname.androidproject.CatalystConstants
|
|
4
|
-
import org.json.JSONException
|
|
5
|
-
import org.json.JSONObject
|
|
6
|
-
import org.junit.Assert.assertEquals
|
|
7
|
-
import org.junit.Assert.assertNull
|
|
8
|
-
import org.junit.Assert.assertTrue
|
|
9
|
-
import org.junit.Assert.fail
|
|
10
|
-
import org.junit.Test
|
|
11
|
-
|
|
12
|
-
class PluginBridgeTest {
|
|
13
|
-
|
|
14
|
-
@Test
|
|
15
|
-
fun `parseRequest accepts valid payload and trims string fields`() {
|
|
16
|
-
val request = PluginBridge.parseRequest(
|
|
17
|
-
"""
|
|
18
|
-
{
|
|
19
|
-
"pluginId": " device-info-plugin ",
|
|
20
|
-
"command": " getDeviceInfo ",
|
|
21
|
-
"data": { "includeSecurity": true },
|
|
22
|
-
"requestId": " req-123 "
|
|
23
|
-
}
|
|
24
|
-
""".trimIndent()
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
assertEquals("device-info-plugin", request.pluginId)
|
|
28
|
-
assertEquals("getDeviceInfo", request.command)
|
|
29
|
-
assertEquals("req-123", request.requestId)
|
|
30
|
-
assertEquals(true, (request.data as JSONObject).getBoolean("includeSecurity"))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Test
|
|
34
|
-
fun `parseRequest treats blank requestId as null`() {
|
|
35
|
-
val request = PluginBridge.parseRequest(
|
|
36
|
-
"""
|
|
37
|
-
{
|
|
38
|
-
"pluginId": "device-info-plugin",
|
|
39
|
-
"command": "getDeviceInfo",
|
|
40
|
-
"requestId": " "
|
|
41
|
-
}
|
|
42
|
-
""".trimIndent()
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
assertNull(request.requestId)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@Test
|
|
49
|
-
fun `parseRequest rejects blank payload`() {
|
|
50
|
-
try {
|
|
51
|
-
PluginBridge.parseRequest(" ")
|
|
52
|
-
fail("Expected invalid blank payload to throw")
|
|
53
|
-
} catch (error: IllegalArgumentException) {
|
|
54
|
-
assertEquals("Payload is required", error.message)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Test
|
|
59
|
-
fun `parseRequest rejects oversized payload`() {
|
|
60
|
-
val oversizedData = "x".repeat(CatalystConstants.Bridge.MAX_MESSAGE_SIZE + 256)
|
|
61
|
-
val payload = """
|
|
62
|
-
{
|
|
63
|
-
"pluginId": "device-info-plugin",
|
|
64
|
-
"command": "getDeviceInfo",
|
|
65
|
-
"data": "$oversizedData"
|
|
66
|
-
}
|
|
67
|
-
""".trimIndent()
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
PluginBridge.parseRequest(payload)
|
|
71
|
-
fail("Expected oversized payload to throw")
|
|
72
|
-
} catch (error: IllegalArgumentException) {
|
|
73
|
-
assertEquals("Payload exceeds maximum size", error.message)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@Test
|
|
78
|
-
fun `parseRequest rejects non string pluginId`() {
|
|
79
|
-
try {
|
|
80
|
-
PluginBridge.parseRequest(
|
|
81
|
-
"""
|
|
82
|
-
{
|
|
83
|
-
"pluginId": 42,
|
|
84
|
-
"command": "getDeviceInfo"
|
|
85
|
-
}
|
|
86
|
-
""".trimIndent()
|
|
87
|
-
)
|
|
88
|
-
fail("Expected non-string pluginId to throw")
|
|
89
|
-
} catch (error: IllegalArgumentException) {
|
|
90
|
-
assertEquals("pluginId must be a string", error.message)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@Test
|
|
95
|
-
fun `parseRequest rejects non string requestId`() {
|
|
96
|
-
try {
|
|
97
|
-
PluginBridge.parseRequest(
|
|
98
|
-
"""
|
|
99
|
-
{
|
|
100
|
-
"pluginId": "device-info-plugin",
|
|
101
|
-
"command": "getDeviceInfo",
|
|
102
|
-
"requestId": 42
|
|
103
|
-
}
|
|
104
|
-
""".trimIndent()
|
|
105
|
-
)
|
|
106
|
-
fail("Expected non-string requestId to throw")
|
|
107
|
-
} catch (error: IllegalArgumentException) {
|
|
108
|
-
assertEquals("requestId must be a string when provided", error.message)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
@Test
|
|
113
|
-
fun `parseRequest rejects invalid JSON`() {
|
|
114
|
-
try {
|
|
115
|
-
PluginBridge.parseRequest("{")
|
|
116
|
-
fail("Expected invalid JSON to throw")
|
|
117
|
-
} catch (error: JSONException) {
|
|
118
|
-
assertTrue(error.message?.isNotBlank() == true)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@Test
|
|
123
|
-
fun `parseRequest rejects non object data`() {
|
|
124
|
-
try {
|
|
125
|
-
PluginBridge.parseRequest(
|
|
126
|
-
"""
|
|
127
|
-
{
|
|
128
|
-
"pluginId": "device-info-plugin",
|
|
129
|
-
"command": "getDeviceInfo",
|
|
130
|
-
"data": "unsafe"
|
|
131
|
-
}
|
|
132
|
-
""".trimIndent()
|
|
133
|
-
)
|
|
134
|
-
fail("Expected non-object data to throw")
|
|
135
|
-
} catch (error: IllegalArgumentException) {
|
|
136
|
-
assertEquals("data must be an object when provided", error.message)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
package io.yourname.androidproject.plugins.internal.deviceinfo
|
|
2
|
-
|
|
3
|
-
import android.util.Log
|
|
4
|
-
import io.yourname.androidproject.plugins.CatalystPlugin
|
|
5
|
-
import io.yourname.androidproject.plugins.PluginBridgeContext
|
|
6
|
-
import io.yourname.androidproject.utils.DeviceInfoUtils
|
|
7
|
-
import org.json.JSONObject
|
|
8
|
-
|
|
9
|
-
class DeviceInfoPlugin : CatalystPlugin {
|
|
10
|
-
companion object {
|
|
11
|
-
private const val TAG = "DeviceInfoPlugin"
|
|
12
|
-
private const val COMMAND_GET_DEVICE_INFO = "getDeviceInfo"
|
|
13
|
-
private const val CALLBACK_SUCCESS = "onSuccess"
|
|
14
|
-
private const val CALLBACK_ERROR = "onError"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
override fun handle(command: String, data: JSONObject?, bridge: PluginBridgeContext) {
|
|
18
|
-
if (command != COMMAND_GET_DEVICE_INFO) {
|
|
19
|
-
bridge.callback(
|
|
20
|
-
CALLBACK_ERROR,
|
|
21
|
-
JSONObject().apply {
|
|
22
|
-
put("message", "Unsupported command: $command")
|
|
23
|
-
put("code", "UNSUPPORTED_COMMAND")
|
|
24
|
-
}
|
|
25
|
-
)
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
val deviceInfo = DeviceInfoUtils.getDeviceInfo(bridge.context, bridge.properties)
|
|
31
|
-
bridge.callback(CALLBACK_SUCCESS, deviceInfo)
|
|
32
|
-
} catch (error: Exception) {
|
|
33
|
-
Log.e(TAG, "Failed to resolve device info", error)
|
|
34
|
-
bridge.callback(
|
|
35
|
-
CALLBACK_ERROR,
|
|
36
|
-
JSONObject().apply {
|
|
37
|
-
put("message", error.message ?: "Failed to get device info")
|
|
38
|
-
put("code", "DEVICE_INFO_ERROR")
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
final class DeviceInfoPlugin: CatalystPlugin {
|
|
4
|
-
private let commandGetDeviceInfo = "getDeviceInfo"
|
|
5
|
-
private let successCallback = "onSuccess"
|
|
6
|
-
private let errorCallback = "onError"
|
|
7
|
-
|
|
8
|
-
func handle(command: String, data: Any?, bridge: PluginBridgeContext) {
|
|
9
|
-
guard command == commandGetDeviceInfo else {
|
|
10
|
-
bridge.callback(eventName: errorCallback, data: [
|
|
11
|
-
"message": "Unsupported command: \(command)",
|
|
12
|
-
"code": "UNSUPPORTED_COMMAND",
|
|
13
|
-
])
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let deviceInfo = DeviceInfoUtils.getDeviceInfo()
|
|
18
|
-
if let error = deviceInfo["error"] as? String {
|
|
19
|
-
bridge.callback(eventName: errorCallback, data: [
|
|
20
|
-
"message": error,
|
|
21
|
-
"code": "DEVICE_INFO_ERROR",
|
|
22
|
-
])
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
bridge.callback(eventName: successCallback, data: deviceInfo)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "io.catalyst.device_info",
|
|
3
|
-
"configKey": "deviceInfo",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"displayName": "Device Info",
|
|
6
|
-
"description": "Provides device metadata, screen metrics, app info, and platform-specific diagnostics.",
|
|
7
|
-
"category": "device",
|
|
8
|
-
"platforms": ["android", "ios"],
|
|
9
|
-
"commands": ["getDeviceInfo"],
|
|
10
|
-
"android": {
|
|
11
|
-
"className": "io.yourname.androidproject.plugins.internal.deviceinfo.DeviceInfoPlugin",
|
|
12
|
-
"permissions": [],
|
|
13
|
-
"dependencies": []
|
|
14
|
-
},
|
|
15
|
-
"ios": {
|
|
16
|
-
"className": "DeviceInfoPlugin",
|
|
17
|
-
"dependencies": []
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function readStringArray(value,fieldName,sourcePath,{required=false,nonEmpty=false}={}){if(!Array.isArray(value)){if(required){throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);}return[];}const result=value.map(entry=>mustBeNonEmptyString(entry,`${fieldName}[]`,sourcePath));if(nonEmpty&&result.length===0){throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);}return result;}function readPlainObject(value,fieldName,sourcePath){if(value==null){return{};}if(!value||typeof value!=="object"||Array.isArray(value)){throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);}return value;}function cloneJsonValue(value,fieldName,sourcePath){try{return JSON.parse(JSON.stringify(value));}catch(error){throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);}}function readIosUrlSchemes(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}return{name:entry.name==null?null:mustBeNonEmptyString(entry.name,`${entryField}.name`,sourcePath),schemes:readStringArray(entry.schemes,`${entryField}.schemes`,sourcePath,{required:true,nonEmpty:true})};});}function readIosDependencies(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}const url=mustBeNonEmptyString(entry.url,`${entryField}.url`,sourcePath);const packageIdentity=entry.package==null?derivePackageIdentityFromUrl(url,`${entryField}.url`,sourcePath):mustBeNonEmptyString(entry.package,`${entryField}.package`,sourcePath);const products=readStringArray(entry.products,`${entryField}.products`,sourcePath,{required:true,nonEmpty:true});if(new Set(products).size!==products.length){throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);}const hasFrom=entry.from!=null;const hasExact=entry.exact!=null;if(hasFrom===hasExact){throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);}return{url,package:packageIdentity,products,requirement:hasFrom?{type:"from",version:mustBeNonEmptyString(entry.from,`${entryField}.from`,sourcePath)}:{type:"exact",version:mustBeNonEmptyString(entry.exact,`${entryField}.exact`,sourcePath)}};});}function derivePackageIdentityFromUrl(url,fieldName,sourcePath){const sanitizedUrl=url.replace(/\/+$/,"");const packageIdentity=sanitizedUrl.split("/").pop()?.replace(/\.git$/,"");if(!packageIdentity){throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);}return packageIdentity;}function parsePluginManifest(pluginDir){const manifestPath=path.join(pluginDir,"manifest.json");if(!fs.existsSync(manifestPath)){return null;}let manifestContent;try{manifestContent=fs.readFileSync(manifestPath,"utf8");}catch(error){throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${error.message}`);}let manifest;try{manifest=JSON.parse(manifestContent);}catch(error){throw new Error(`Invalid JSON in plugin manifest ${manifestPath}: ${error.message}`);}const id=mustBeNonEmptyString(manifest.id,"id",manifestPath);const configKey=mustBeNonEmptyString(manifest.configKey,"configKey",manifestPath);const platforms=readStringArray(manifest.platforms,"platforms",manifestPath,{required:true,nonEmpty:true});const androidConfig=platforms.includes("android")?manifest.android:null;const iosConfig=platforms.includes("ios")?manifest.ios:null;if(platforms.includes("android")&&(!androidConfig||typeof androidConfig!=="object")){throw new Error(`'android' config is required for plugin '${id}' in ${manifestPath}`);}if(platforms.includes("ios")&&(!iosConfig||typeof iosConfig!=="object")){throw new Error(`'ios' config is required for plugin '${id}' in ${manifestPath}`);}return{pluginDir,manifestPath,id,configKey,version:mustBeNonEmptyString(manifest.version,"version",manifestPath),displayName:mustBeNonEmptyString(manifest.displayName,"displayName",manifestPath),description:mustBeNonEmptyString(manifest.description,"description",manifestPath),category:mustBeNonEmptyString(manifest.category,"category",manifestPath),platforms,commands:readStringArray(manifest.commands,"commands",manifestPath,{required:true,nonEmpty:true}),android:androidConfig?{sourceDir:path.join(pluginDir,"android"),permissions:readStringArray(androidConfig.permissions,"android.permissions",manifestPath),dependencies:readStringArray(androidConfig.dependencies,"android.dependencies",manifestPath),className:mustBeNonEmptyString(androidConfig.className,"android.className",manifestPath)}:null,ios:iosConfig?{sourceDir:path.join(pluginDir,"ios"),dependencies:readIosDependencies(iosConfig.dependencies,"ios.dependencies",manifestPath),className:mustBeNonEmptyString(iosConfig.className,"ios.className",manifestPath),infoPlist:cloneJsonValue(readPlainObject(iosConfig.infoPlist,"ios.infoPlist",manifestPath),"ios.infoPlist",manifestPath),urlSchemes:readIosUrlSchemes(iosConfig.urlSchemes,"ios.urlSchemes",manifestPath),querySchemes:readStringArray(iosConfig.querySchemes,"ios.querySchemes",manifestPath),entitlements:cloneJsonValue(readPlainObject(iosConfig.entitlements,"ios.entitlements",manifestPath),"ios.entitlements",manifestPath),resources:readStringArray(iosConfig.resources,"ios.resources",manifestPath)}:null};}function discoverInternalPlugins(corePluginsRoot,log=()=>{}){if(!corePluginsRoot||!isDir(corePluginsRoot)){log(`No internal plugin directory found at ${corePluginsRoot||"<empty>"}`,"info");return[];}const plugins=[];const entries=fs.readdirSync(corePluginsRoot,{withFileTypes:true}).sort((left,right)=>left.name.localeCompare(right.name));for(const entry of entries){if(!entry.isDirectory()){continue;}const pluginDir=path.join(corePluginsRoot,entry.name);const parsed=parsePluginManifest(pluginDir);if(parsed){plugins.push(parsed);}}log(`Discovered ${plugins.length} internal plugin manifest(s)`,"info");return plugins;}function resolveInternalPluginsRoot(packageRoot){const distPluginsPath=path.join(packageRoot,"dist","native","internal-plugins");const srcPluginsPath=path.join(packageRoot,"src","native","internal-plugins");return fs.existsSync(distPluginsPath)?distPluginsPath:srcPluginsPath;}function resolvePluginConfig(WEBVIEW_CONFIG){const pluginConfig={};if(WEBVIEW_CONFIG.plugins!=null){if(typeof WEBVIEW_CONFIG.plugins!=="object"||Array.isArray(WEBVIEW_CONFIG.plugins)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}for(const[key,value]of Object.entries(WEBVIEW_CONFIG.plugins)){if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${key}' must be boolean`);}pluginConfig[key]=value;}}return pluginConfig;}module.exports={discoverInternalPlugins,parsePluginManifest,resolvePluginConfig,resolveInternalPluginsRoot};
|