@granite-js/micro-frontend 2.3.0 → 2.3.2
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/CHANGELOG.md +14 -0
- package/README.md +5 -3
- package/android/src/main/cpp/BundleEvaluator.cpp +24 -1
- package/android/src/main/cpp/BundleEvaluator.h +6 -0
- package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +50 -10
- package/android/src/test/java/run/granite/microfrontend/BundleEvaluatorTest.kt +42 -0
- package/dist/module/createRoute.js +3 -3
- package/dist/module/createRoute.js.map +1 -1
- package/dist/typescript/types.d.ts +1 -0
- package/package.json +4 -4
- package/src/createRoute.ts +2 -4
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @granite-js/micro-frontend
|
|
2
2
|
|
|
3
|
+
## 2.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f92bbc7: Register pending host components with the configured app scheme so route evaluation works before an initial URL is available and does not inherit another app's scheme.
|
|
8
|
+
- @granite-js/utils@2.3.2
|
|
9
|
+
|
|
10
|
+
## 2.3.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 56a5ce8: Support evaluating Android bundles packaged under `assets://` locators.
|
|
15
|
+
- @granite-js/utils@2.3.1
|
|
16
|
+
|
|
3
17
|
## 2.3.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -168,7 +168,8 @@ app name exactly rather than routing it to another remote.
|
|
|
168
168
|
## JavaScript runtime
|
|
169
169
|
|
|
170
170
|
The adapter owns bundle selection, download, integrity verification, and
|
|
171
|
-
caching. It returns an absolute local bundle path
|
|
171
|
+
caching. It returns an absolute local bundle path, or an Android `assets://`
|
|
172
|
+
locator for a bundle packaged in the application.
|
|
172
173
|
|
|
173
174
|
```ts
|
|
174
175
|
import { createMicroFrontendRuntime } from '@granite-js/micro-frontend';
|
|
@@ -220,7 +221,7 @@ The public runtime API is:
|
|
|
220
221
|
| -------------------------- | --------------------------------------------------------------- |
|
|
221
222
|
| `preloadApp(appName)` | Load and evaluate one app without importing an exposed module. |
|
|
222
223
|
| `importApp(request)` | Ensure the app is evaluated and import `appName/exposedModule`. |
|
|
223
|
-
| `evaluateScript(filePath)` | Evaluate
|
|
224
|
+
| `evaluateScript(filePath)` | Evaluate a local file or Android packaged asset in the retained runtime. |
|
|
224
225
|
| `onEvent(listener)` | Subscribe to native open, close, and visibility events. |
|
|
225
226
|
|
|
226
227
|
The host can provide `onLifecycleEvent` when creating the runtime for logging
|
|
@@ -371,7 +372,8 @@ The brownfield application must:
|
|
|
371
372
|
boundary;
|
|
372
373
|
4. bind native screen lifecycle to the session APIs below;
|
|
373
374
|
5. install a Portal destination with the same `sessionId`;
|
|
374
|
-
6. provide `adapter.loadBundle()` and return an absolute verified bundle path
|
|
375
|
+
6. provide `adapter.loadBundle()` and return an absolute verified bundle path,
|
|
376
|
+
or an Android `assets://` locator for a verified packaged bundle;
|
|
375
377
|
7. keep the React tree mounted until native teardown emits `closeApp`; and
|
|
376
378
|
8. keep Granite's base brownfield view APIs, such as scheme resolution and
|
|
377
379
|
closing the current Granite view, in the Granite host integration. They are
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
namespace granite::microfrontend {
|
|
7
7
|
|
|
8
|
+
using facebook::jni::JArrayByte;
|
|
8
9
|
using facebook::jni::JString;
|
|
9
10
|
using facebook::jni::alias_ref;
|
|
10
11
|
using facebook::jsi::Runtime;
|
|
@@ -12,7 +13,29 @@ using facebook::jsi::StringBuffer;
|
|
|
12
13
|
|
|
13
14
|
void BundleEvaluator::registerNatives() {
|
|
14
15
|
registerHybrid(
|
|
15
|
-
{makeNativeMethod("evaluateFileSync", BundleEvaluator::evaluateFileSync)
|
|
16
|
+
{makeNativeMethod("evaluateFileSync", BundleEvaluator::evaluateFileSync),
|
|
17
|
+
makeNativeMethod("evaluateScriptSync", BundleEvaluator::evaluateScriptSync)});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
void BundleEvaluator::evaluateScriptSync(
|
|
21
|
+
alias_ref<jhybridobject>,
|
|
22
|
+
jlong runtimePointer,
|
|
23
|
+
alias_ref<JArrayByte> scriptData,
|
|
24
|
+
alias_ref<JString> sourceUrl) {
|
|
25
|
+
if (runtimePointer == 0) {
|
|
26
|
+
facebook::jni::throwNewJavaException(
|
|
27
|
+
"java/lang/IllegalStateException", "JavaScript runtime is unavailable");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
auto pinnedScriptData = scriptData->pin();
|
|
32
|
+
std::string source{
|
|
33
|
+
reinterpret_cast<const char *>(pinnedScriptData.get()),
|
|
34
|
+
pinnedScriptData.size()};
|
|
35
|
+
auto *runtime = reinterpret_cast<Runtime *>(runtimePointer);
|
|
36
|
+
runtime->evaluateJavaScript(
|
|
37
|
+
std::make_shared<StringBuffer>(std::move(source)),
|
|
38
|
+
sourceUrl->toStdString());
|
|
16
39
|
}
|
|
17
40
|
|
|
18
41
|
void BundleEvaluator::evaluateFileSync(
|
|
@@ -15,6 +15,12 @@ struct BundleEvaluator : facebook::jni::HybridClass<BundleEvaluator> {
|
|
|
15
15
|
jlong runtimePointer,
|
|
16
16
|
facebook::jni::alias_ref<facebook::jni::JString> filePath,
|
|
17
17
|
facebook::jni::alias_ref<facebook::jni::JString> sourceUrl);
|
|
18
|
+
|
|
19
|
+
static void evaluateScriptSync(
|
|
20
|
+
facebook::jni::alias_ref<jhybridobject>,
|
|
21
|
+
jlong runtimePointer,
|
|
22
|
+
facebook::jni::alias_ref<facebook::jni::JArrayByte> scriptData,
|
|
23
|
+
facebook::jni::alias_ref<facebook::jni::JString> sourceUrl);
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
} // namespace granite::microfrontend
|
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
package run.granite.microfrontend
|
|
2
2
|
|
|
3
|
+
import android.content.res.AssetManager
|
|
3
4
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
5
|
import com.facebook.react.common.annotations.FrameworkAPI
|
|
5
6
|
import java.io.File
|
|
6
7
|
import java.io.FileNotFoundException
|
|
7
8
|
import java.io.IOException
|
|
8
9
|
|
|
10
|
+
internal sealed interface ResolvedBundleSource {
|
|
11
|
+
val sourceUrl: String
|
|
12
|
+
|
|
13
|
+
data class Asset(
|
|
14
|
+
val scriptData: ByteArray,
|
|
15
|
+
override val sourceUrl: String,
|
|
16
|
+
) : ResolvedBundleSource
|
|
17
|
+
|
|
18
|
+
data class FilePath(
|
|
19
|
+
val filePath: String,
|
|
20
|
+
override val sourceUrl: String,
|
|
21
|
+
) : ResolvedBundleSource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal fun resolveBundleSource(assetManager: AssetManager, bundlePath: String): ResolvedBundleSource {
|
|
25
|
+
if (bundlePath.startsWith(ASSET_PREFIX)) {
|
|
26
|
+
val assetPath = bundlePath.removePrefix(ASSET_PREFIX)
|
|
27
|
+
require(assetPath.isNotBlank()) { "Bundle asset path must not be blank: $bundlePath" }
|
|
28
|
+
val scriptData = assetManager.open(assetPath).use { it.readBytes() }
|
|
29
|
+
if (scriptData.isEmpty()) {
|
|
30
|
+
throw IOException("Bundle asset is empty: $bundlePath")
|
|
31
|
+
}
|
|
32
|
+
return ResolvedBundleSource.Asset(scriptData, bundlePath)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
val file = File(bundlePath)
|
|
36
|
+
require(file.isAbsolute) { "Bundle path must be absolute: $bundlePath" }
|
|
37
|
+
if (!file.exists()) {
|
|
38
|
+
throw FileNotFoundException("Bundle file does not exist: $bundlePath")
|
|
39
|
+
}
|
|
40
|
+
if (!file.canRead()) {
|
|
41
|
+
throw IOException("Bundle file is not readable: $bundlePath")
|
|
42
|
+
}
|
|
43
|
+
return ResolvedBundleSource.FilePath(bundlePath, file.toURI().toString())
|
|
44
|
+
}
|
|
45
|
+
|
|
9
46
|
@OptIn(FrameworkAPI::class)
|
|
10
47
|
internal class BundleEvaluator(
|
|
11
48
|
private val reactContext: ReactApplicationContext,
|
|
@@ -22,20 +59,21 @@ internal class BundleEvaluator(
|
|
|
22
59
|
sourceUrl: String,
|
|
23
60
|
)
|
|
24
61
|
|
|
62
|
+
private external fun evaluateScriptSync(
|
|
63
|
+
runtimePointer: Long,
|
|
64
|
+
scriptData: ByteArray,
|
|
65
|
+
sourceUrl: String,
|
|
66
|
+
)
|
|
67
|
+
|
|
25
68
|
@Throws(FileNotFoundException::class, IOException::class)
|
|
26
69
|
fun evaluateFile(filePath: String) {
|
|
27
|
-
val
|
|
28
|
-
require(file.isAbsolute) { "Bundle path must be absolute: $filePath" }
|
|
29
|
-
if (!file.exists()) {
|
|
30
|
-
throw FileNotFoundException("Bundle file does not exist: $filePath")
|
|
31
|
-
}
|
|
32
|
-
if (!file.canRead()) {
|
|
33
|
-
throw IOException("Bundle file is not readable: $filePath")
|
|
34
|
-
}
|
|
35
|
-
|
|
70
|
+
val source = resolveBundleSource(reactContext.assets, filePath)
|
|
36
71
|
val runtimePointer = reactContext.javaScriptContextHolder?.get()
|
|
37
72
|
?: throw IllegalStateException("JavaScript runtime is unavailable")
|
|
38
|
-
|
|
73
|
+
when (source) {
|
|
74
|
+
is ResolvedBundleSource.Asset -> evaluateScriptSync(runtimePointer, source.scriptData, source.sourceUrl)
|
|
75
|
+
is ResolvedBundleSource.FilePath -> evaluateFileSync(runtimePointer, source.filePath, source.sourceUrl)
|
|
76
|
+
}
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
private companion object {
|
|
@@ -44,3 +82,5 @@ internal class BundleEvaluator(
|
|
|
44
82
|
}
|
|
45
83
|
}
|
|
46
84
|
}
|
|
85
|
+
|
|
86
|
+
private const val ASSET_PREFIX = "assets://"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
import android.content.res.AssetManager
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.mockito.Mockito.mock
|
|
9
|
+
import org.mockito.Mockito.verify
|
|
10
|
+
import org.mockito.Mockito.`when`
|
|
11
|
+
import java.io.ByteArrayInputStream
|
|
12
|
+
import java.io.File
|
|
13
|
+
|
|
14
|
+
class BundleEvaluatorTest {
|
|
15
|
+
@Test
|
|
16
|
+
fun `asset bundle locator reads the built-in bundle through AssetManager`() {
|
|
17
|
+
val assetManager = mock(AssetManager::class.java)
|
|
18
|
+
val scriptData = "built-in bundle".toByteArray()
|
|
19
|
+
`when`(assetManager.open("bundles/example.hbc")).thenReturn(ByteArrayInputStream(scriptData))
|
|
20
|
+
|
|
21
|
+
val source = resolveBundleSource(assetManager, "assets://bundles/example.hbc")
|
|
22
|
+
|
|
23
|
+
assertTrue(source is ResolvedBundleSource.Asset)
|
|
24
|
+
assertArrayEquals(scriptData, (source as ResolvedBundleSource.Asset).scriptData)
|
|
25
|
+
assertEquals("assets://bundles/example.hbc", source.sourceUrl)
|
|
26
|
+
verify(assetManager).open("bundles/example.hbc")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `absolute file path keeps filesystem bundle evaluation`() {
|
|
31
|
+
val bundleFile = File.createTempFile("granite-micro-frontend", ".hbc")
|
|
32
|
+
try {
|
|
33
|
+
val source = resolveBundleSource(mock(AssetManager::class.java), bundleFile.absolutePath)
|
|
34
|
+
|
|
35
|
+
assertTrue(source is ResolvedBundleSource.FilePath)
|
|
36
|
+
assertEquals(bundleFile.absolutePath, (source as ResolvedBundleSource.FilePath).filePath)
|
|
37
|
+
assertEquals(bundleFile.toURI().toString(), source.sourceUrl)
|
|
38
|
+
} finally {
|
|
39
|
+
bundleFile.delete()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { createRoute as createGraniteRoute
|
|
3
|
+
import { createRoute as createGraniteRoute } from '@granite-js/react-native';
|
|
4
4
|
import { registerPendingHostComponentRoute } from "./host/pendingHostComponentStore.js";
|
|
5
5
|
export function createRoute(path, options) {
|
|
6
6
|
const {
|
|
@@ -27,10 +27,10 @@ function getCurrentGraniteApp() {
|
|
|
27
27
|
}
|
|
28
28
|
const host = granite['app']['host'];
|
|
29
29
|
const name = granite['app']['name'];
|
|
30
|
-
|
|
30
|
+
const scheme = granite['app']['scheme'];
|
|
31
|
+
if (typeof host !== 'string' || typeof name !== 'string' || typeof scheme !== 'string') {
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
|
-
const scheme = new URL(getSchemeUri()).protocol.replace(/:$/g, '');
|
|
34
34
|
return {
|
|
35
35
|
name,
|
|
36
36
|
scheme,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["createRoute","createGraniteRoute","
|
|
1
|
+
{"version":3,"names":["createRoute","createGraniteRoute","registerPendingHostComponentRoute","path","options","hostPendingComponent","routeOptions","app","getCurrentGraniteApp","String","component","parserParams","validateParams","granite","Reflect","get","globalThis","isPropertyMap","host","name","scheme","value"],"sourceRoot":"../../src","sources":["createRoute.ts"],"mappings":";;AAAA,SACEA,WAAW,IAAIC,kBAAkB,QAI5B,0BAA0B;AAEjC,SAASC,iCAAiC,QAAQ,qCAAkC;AA+BpF,OAAO,SAASF,WAAWA,CACzBG,IAA+B,EAC/BC,OAA2C,EAC3C;EACA,MAAM;IAAEC,oBAAoB;IAAE,GAAGC;EAAa,CAAC,GAAGF,OAAO;EAEzD,IAAIC,oBAAoB,IAAI,IAAI,EAAE;IAChC,MAAME,GAAG,GAAGC,oBAAoB,CAAC,CAAC;IAClC,IAAID,GAAG,IAAI,IAAI,EAAE;MACfL,iCAAiC,CAACO,MAAM,CAACN,IAAI,CAAC,EAAE;QAC9CI,GAAG;QACHG,SAAS,EAAEL,oBAAoB;QAC/BM,YAAY,EAAEL,YAAY,CAACK,YAAY;QACvCC,cAAc,EAAEN,YAAY,CAACM;MAC/B,CAAC,CAAC;IACJ;EACF;EAEA,OAAOX,kBAAkB,CAACE,IAAI,EAAEG,YAAY,CAAC;AAC/C;AAEA,SAASE,oBAAoBA,CAAA,EAAyC;EACpE,MAAMK,OAAgB,GAAGC,OAAO,CAACC,GAAG,CAACC,UAAU,EAAE,WAAW,CAAC;EAC7D,IAAI,CAACC,aAAa,CAACJ,OAAO,CAAC,IAAI,CAACI,aAAa,CAACJ,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE;IAC7D,OAAO,IAAI;EACb;EAEA,MAAMK,IAAI,GAAGL,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,MAAMM,IAAI,GAAGN,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,MAAMO,MAAM,GAAGP,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC;EACvC,IAAI,OAAOK,IAAI,KAAK,QAAQ,IAAI,OAAOC,IAAI,KAAK,QAAQ,IAAI,OAAOC,MAAM,KAAK,QAAQ,EAAE;IACtF,OAAO,IAAI;EACb;EAEA,OAAO;IACLD,IAAI;IACJC,MAAM;IACNF;EACF,CAAC;AACH;AAEA,SAASD,aAAaA,CAACI,KAAc,EAAmD;EACtF,OAAO,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI;AACpD","ignoreList":[]}
|
|
@@ -3,6 +3,7 @@ export interface MicroFrontendBundleRequest {
|
|
|
3
3
|
readonly appName: string;
|
|
4
4
|
}
|
|
5
5
|
export interface MicroFrontendBundle {
|
|
6
|
+
/** An absolute local file path, or an Android `assets://` locator for a packaged bundle. */
|
|
6
7
|
readonly filePath: string;
|
|
7
8
|
}
|
|
8
9
|
export interface MicroFrontendAppProps {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@granite-js/micro-frontend",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Micro-frontend build, runtime, session, and Portal contracts for Granite brownfield applications",
|
|
5
5
|
"main": "./dist/module/index.js",
|
|
6
6
|
"types": "./dist/typescript/index.d.ts",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@babel/core": "7.28.5",
|
|
59
|
-
"@granite-js/utils": "2.3.
|
|
59
|
+
"@granite-js/utils": "2.3.2",
|
|
60
60
|
"@standard-schema/spec": "^1.0.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@babel/types": "7.28.5",
|
|
75
|
-
"@granite-js/plugin-core": "2.3.
|
|
76
|
-
"@granite-js/react-native": "2.3.
|
|
75
|
+
"@granite-js/plugin-core": "2.3.2",
|
|
76
|
+
"@granite-js/react-native": "2.3.2",
|
|
77
77
|
"@types/babel__core": "^7.20.5",
|
|
78
78
|
"@types/node": "24.10.12",
|
|
79
79
|
"@types/react": "19.2.0",
|
package/src/createRoute.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRoute as createGraniteRoute,
|
|
3
|
-
getSchemeUri,
|
|
4
3
|
type RegisterScreenInput,
|
|
5
4
|
type RouteOptions,
|
|
6
5
|
useNavigation,
|
|
@@ -66,12 +65,11 @@ function getCurrentGraniteApp(): PendingHostComponentAppConfig | null {
|
|
|
66
65
|
|
|
67
66
|
const host = granite['app']['host'];
|
|
68
67
|
const name = granite['app']['name'];
|
|
69
|
-
|
|
68
|
+
const scheme = granite['app']['scheme'];
|
|
69
|
+
if (typeof host !== 'string' || typeof name !== 'string' || typeof scheme !== 'string') {
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const scheme = new URL(getSchemeUri()).protocol.replace(/:$/g, '');
|
|
74
|
-
|
|
75
73
|
return {
|
|
76
74
|
name,
|
|
77
75
|
scheme,
|