@elliemae/pui-app-bridge 2.25.0 → 2.26.0-alpha.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/dist/cjs/appBridge.js +177 -26
- package/dist/cjs/config/app.js +8 -0
- package/dist/cjs/frame.html +0 -21
- package/dist/cjs/loaders/script.js +12 -12
- package/dist/cjs/tests/serverHandlers.js +0 -8
- package/dist/esm/appBridge.js +177 -26
- package/dist/esm/config/app.js +8 -0
- package/dist/esm/frame.html +0 -21
- package/dist/esm/loaders/script.js +12 -12
- package/dist/esm/tests/serverHandlers.js +0 -8
- package/dist/public/assets/{frame.671d9de68be598da64ca.html → frame.4cbbcfa9ded96b660559.html} +0 -21
- package/dist/public/e2e-host.html +1 -1
- package/dist/public/e2e-index.html +1 -1
- package/dist/public/frame.html +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/js/emuiAppBridge.2cb0198671d2a7602aaa.js +17 -0
- package/dist/public/js/emuiAppBridge.2cb0198671d2a7602aaa.js.br +0 -0
- package/dist/public/js/emuiAppBridge.2cb0198671d2a7602aaa.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.2cb0198671d2a7602aaa.js.map +1 -0
- package/dist/types/lib/appBridge.d.ts +11 -2
- package/dist/types/lib/config/app.d.ts +6 -0
- package/dist/types/lib/loaders/script.d.ts +5 -3
- package/dist/types/lib/typings/host.d.ts +5 -0
- package/dist/types/lib/typings/window.d.ts +2 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/umd/{671d9de68be598da64ca.html → 4cbbcfa9ded96b660559.html} +0 -21
- package/dist/umd/frame.html +1 -1
- package/dist/umd/index.js +7 -7
- package/dist/umd/index.js.br +0 -0
- package/dist/umd/index.js.gz +0 -0
- package/dist/umd/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/public/js/emuiAppBridge.87b9f6d6c712609094fd.js +0 -17
- package/dist/public/js/emuiAppBridge.87b9f6d6c712609094fd.js.br +0 -0
- package/dist/public/js/emuiAppBridge.87b9f6d6c712609094fd.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.87b9f6d6c712609094fd.js.map +0 -1
package/dist/cjs/appBridge.js
CHANGED
|
@@ -153,18 +153,19 @@ class CAppBridge {
|
|
|
153
153
|
};
|
|
154
154
|
/**
|
|
155
155
|
* add app to active app list
|
|
156
|
-
* @param
|
|
157
|
-
* @param
|
|
158
|
-
* @param
|
|
159
|
-
* @param
|
|
160
|
-
* @param
|
|
161
|
-
* @param
|
|
156
|
+
* @param options
|
|
157
|
+
* @param options.id - application id
|
|
158
|
+
* @param options.instanceId - unique instance id
|
|
159
|
+
* @param options.documentEle - iframe document
|
|
160
|
+
* @param options.elementIds - ids of injected script/style elements
|
|
161
|
+
* @param options.microFEHost - pre-created host reference for guest-initiated loading
|
|
162
162
|
*/
|
|
163
163
|
#addAppToActiveAppList = ({
|
|
164
164
|
id,
|
|
165
165
|
instanceId,
|
|
166
166
|
documentEle,
|
|
167
|
-
elementIds
|
|
167
|
+
elementIds,
|
|
168
|
+
microFEHost
|
|
168
169
|
}) => {
|
|
169
170
|
const app = this.#appRegistry.get({ id, instanceId });
|
|
170
171
|
if (!app) {
|
|
@@ -177,7 +178,8 @@ class CAppBridge {
|
|
|
177
178
|
guest: {
|
|
178
179
|
guestWindow: documentEle?.defaultView,
|
|
179
180
|
...app
|
|
180
|
-
}
|
|
181
|
+
},
|
|
182
|
+
microFEHost
|
|
181
183
|
});
|
|
182
184
|
};
|
|
183
185
|
#initApplication = async ({
|
|
@@ -189,7 +191,8 @@ class CAppBridge {
|
|
|
189
191
|
homeRoute,
|
|
190
192
|
initialRoute,
|
|
191
193
|
history,
|
|
192
|
-
theme
|
|
194
|
+
theme,
|
|
195
|
+
microFEHost
|
|
193
196
|
}) => {
|
|
194
197
|
const app = this.#appRegistry.get({ id, instanceId });
|
|
195
198
|
if (!app) {
|
|
@@ -201,10 +204,8 @@ class CAppBridge {
|
|
|
201
204
|
throw new Error(
|
|
202
205
|
`Application ${id} with instance id ${instanceId} doesn't expose init method`
|
|
203
206
|
);
|
|
204
|
-
const host = new import_microfeHost.CMicroFEHost({
|
|
205
|
-
guest: {
|
|
206
|
-
id
|
|
207
|
-
},
|
|
207
|
+
const host = microFEHost ?? new import_microfeHost.CMicroFEHost({
|
|
208
|
+
guest: { id },
|
|
208
209
|
version: this.#version,
|
|
209
210
|
containerId,
|
|
210
211
|
logger: this.#logger,
|
|
@@ -232,7 +233,7 @@ class CAppBridge {
|
|
|
232
233
|
`Application ${id} with instance id ${instanceId} is loading...`
|
|
233
234
|
);
|
|
234
235
|
let assets = files;
|
|
235
|
-
const manifest = await import_loaders.ManifestLoader.get(options);
|
|
236
|
+
const manifest = options.manifest ?? await import_loaders.ManifestLoader.get(options);
|
|
236
237
|
assets = import_loaders.ManifestLoader.getFullFileNameofAssets(manifest, files);
|
|
237
238
|
const cssAssets = assets.filter((fileName) => isCss(fileName));
|
|
238
239
|
const jsAssets = assets.filter((fileName) => !isCss(fileName));
|
|
@@ -250,9 +251,12 @@ class CAppBridge {
|
|
|
250
251
|
id,
|
|
251
252
|
instanceId,
|
|
252
253
|
documentEle,
|
|
253
|
-
elementIds: [...styleElementIds, ...scriptElementIds]
|
|
254
|
+
elementIds: [...styleElementIds, ...scriptElementIds],
|
|
255
|
+
microFEHost: options.microFEHost
|
|
254
256
|
});
|
|
255
|
-
|
|
257
|
+
if (!options.selfInitialize) {
|
|
258
|
+
await this.#initApplication(options);
|
|
259
|
+
}
|
|
256
260
|
this.#logger.audit(
|
|
257
261
|
`Application ${id} with instance id ${instanceId} loaded`
|
|
258
262
|
);
|
|
@@ -351,6 +355,56 @@ class CAppBridge {
|
|
|
351
355
|
});
|
|
352
356
|
}
|
|
353
357
|
};
|
|
358
|
+
/**
|
|
359
|
+
* If the guest's module SO exposes _setUnloadHandler (duck-typed),
|
|
360
|
+
* bind it to closeApp so the guest can trigger its own teardown.
|
|
361
|
+
* @param id
|
|
362
|
+
* @param instanceId
|
|
363
|
+
*/
|
|
364
|
+
#bindModuleUnload = (id, instanceId) => {
|
|
365
|
+
const moduleSO = this.#soManager.getObject(
|
|
366
|
+
import_pui_scripting_object.ScriptingObjectNames.Module,
|
|
367
|
+
{ id }
|
|
368
|
+
);
|
|
369
|
+
const so = moduleSO;
|
|
370
|
+
const handler = so?._setUnloadHandler;
|
|
371
|
+
if (typeof handler === "function") {
|
|
372
|
+
handler(
|
|
373
|
+
() => this.closeApp(instanceId)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Dispatch the module.unloading event and collect feedback.
|
|
379
|
+
* Returns true if unload is allowed, false if any listener vetoed.
|
|
380
|
+
* @param id
|
|
381
|
+
*/
|
|
382
|
+
#canUnload = async (id) => {
|
|
383
|
+
const moduleSO = this.#soManager.getObject(
|
|
384
|
+
import_pui_scripting_object.ScriptingObjectNames.Module,
|
|
385
|
+
{ id }
|
|
386
|
+
);
|
|
387
|
+
if (!moduleSO) return true;
|
|
388
|
+
try {
|
|
389
|
+
const feedback = await this.#eventManager.dispatchEvent(moduleSO, {
|
|
390
|
+
event: { id: "module.unloading", name: "unloading" },
|
|
391
|
+
eventParams: { moduleId: id },
|
|
392
|
+
eventOptions: { timeout: 1e3 }
|
|
393
|
+
});
|
|
394
|
+
if (Array.isArray(feedback) && feedback.some((v) => v === false)) {
|
|
395
|
+
this.#logger.info(
|
|
396
|
+
`Unload denied by guest ${id} via module.unloading event`
|
|
397
|
+
);
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
this.#logger.warn({
|
|
402
|
+
message: `Error dispatching module.unloading event for ${id}`,
|
|
403
|
+
exception: err
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
354
408
|
/**
|
|
355
409
|
* clear session management for the guest application
|
|
356
410
|
* @param instanceId unique instance id of the application
|
|
@@ -483,6 +537,7 @@ class CAppBridge {
|
|
|
483
537
|
return;
|
|
484
538
|
}
|
|
485
539
|
const { id } = app;
|
|
540
|
+
if (!await this.#canUnload(id)) return;
|
|
486
541
|
const appConfig = this.#microFEConfig.getConfigById(id);
|
|
487
542
|
if (!appConfig) {
|
|
488
543
|
this.#logger.warn(`Configuration for application ${id} is not found`);
|
|
@@ -543,14 +598,46 @@ class CAppBridge {
|
|
|
543
598
|
*/
|
|
544
599
|
getApps = () => [...this.#activeApps.values()].map((app) => app.guest);
|
|
545
600
|
/**
|
|
546
|
-
* Initialize appBridge
|
|
601
|
+
* Initialize appBridge.
|
|
602
|
+
* @param preloadedAppConfig - Already-parsed config object. When provided
|
|
603
|
+
* the bridge skips its own HTTP fetch of app.config.json.
|
|
547
604
|
*/
|
|
548
|
-
init = async () => {
|
|
549
|
-
|
|
605
|
+
init = async (preloadedAppConfig) => {
|
|
606
|
+
if (preloadedAppConfig) {
|
|
607
|
+
this.#appConfig.setPreloadedConfig(preloadedAppConfig);
|
|
608
|
+
} else {
|
|
609
|
+
await this.#appConfig.load();
|
|
610
|
+
}
|
|
550
611
|
this.#microFEConfig.init({
|
|
551
612
|
version: this.#version,
|
|
552
613
|
appConfig: this.#appConfig
|
|
553
614
|
});
|
|
615
|
+
this.#injectPreconnects();
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* Inject `<link rel="preconnect">` tags into the parent document for each
|
|
619
|
+
* unique micro-app origin. Called once at the end of {@link init} so that
|
|
620
|
+
* DNS/TCP/TLS handshakes start before any iframe is created.
|
|
621
|
+
*/
|
|
622
|
+
#injectPreconnects = () => {
|
|
623
|
+
const apps = this.#appConfig.get("microFrontendApps") ?? {};
|
|
624
|
+
const origins = /* @__PURE__ */ new Set();
|
|
625
|
+
Object.keys(apps).forEach((id) => {
|
|
626
|
+
const cfg = this.#microFEConfig.getConfigById(id);
|
|
627
|
+
if (cfg?.hostUrl) {
|
|
628
|
+
try {
|
|
629
|
+
origins.add(new URL(cfg.hostUrl).origin);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
origins.forEach((origin) => {
|
|
635
|
+
const link = document.createElement("link");
|
|
636
|
+
link.rel = "preconnect";
|
|
637
|
+
link.href = origin;
|
|
638
|
+
link.crossOrigin = "anonymous";
|
|
639
|
+
document.head.appendChild(link);
|
|
640
|
+
});
|
|
554
641
|
};
|
|
555
642
|
/**
|
|
556
643
|
* Mount guest micro frontend application into DOM
|
|
@@ -571,10 +658,46 @@ class CAppBridge {
|
|
|
571
658
|
}
|
|
572
659
|
await this.#mountApp({ ...appConfig, instanceId });
|
|
573
660
|
};
|
|
661
|
+
/**
|
|
662
|
+
* Prepare the iframe: create a CMicroFEHost, expose it on window.emui,
|
|
663
|
+
* and inject a preconnect hint for the guest origin.
|
|
664
|
+
* @param frameDoc
|
|
665
|
+
* @param appConfig
|
|
666
|
+
* @param appConfig.hostUrl
|
|
667
|
+
* @param options
|
|
668
|
+
* @param options.id
|
|
669
|
+
* @param options.containerId
|
|
670
|
+
*/
|
|
671
|
+
#setupIframe = (frameDoc, appConfig, options) => {
|
|
672
|
+
const microFEHost = new import_microfeHost.CMicroFEHost({
|
|
673
|
+
guest: { id: options.id },
|
|
674
|
+
version: this.#version,
|
|
675
|
+
containerId: options.containerId,
|
|
676
|
+
logger: this.#logger,
|
|
677
|
+
soManager: this.#soManager,
|
|
678
|
+
eventManager: this.#eventManager
|
|
679
|
+
});
|
|
680
|
+
const iframeWindow = frameDoc.defaultView;
|
|
681
|
+
if (iframeWindow) {
|
|
682
|
+
iframeWindow.emui = iframeWindow.emui ?? {};
|
|
683
|
+
iframeWindow.emui.__host = microFEHost;
|
|
684
|
+
iframeWindow.emui.getHost = () => (
|
|
685
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
686
|
+
iframeWindow.emui?.__host ?? null
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const preconnect = frameDoc.createElement("link");
|
|
690
|
+
preconnect.rel = "preconnect";
|
|
691
|
+
preconnect.href = appConfig.hostUrl;
|
|
692
|
+
preconnect.crossOrigin = "anonymous";
|
|
693
|
+
frameDoc.head.appendChild(preconnect);
|
|
694
|
+
return microFEHost;
|
|
695
|
+
};
|
|
574
696
|
/**
|
|
575
697
|
* Open guest micro frontend application
|
|
576
698
|
* @param {OpenAppParams} params - options to open guest application
|
|
577
699
|
*/
|
|
700
|
+
// eslint-disable-next-line max-statements
|
|
578
701
|
openApp = async (params) => {
|
|
579
702
|
const {
|
|
580
703
|
id,
|
|
@@ -583,7 +706,8 @@ class CAppBridge {
|
|
|
583
706
|
theme,
|
|
584
707
|
homeRoute,
|
|
585
708
|
initialRoute,
|
|
586
|
-
metadata
|
|
709
|
+
metadata,
|
|
710
|
+
selfInitialize
|
|
587
711
|
} = params;
|
|
588
712
|
const instanceId = (0, import_uuid.v4)();
|
|
589
713
|
if (metadata) this.#guestMetadata.set(id, metadata);
|
|
@@ -591,15 +715,15 @@ class CAppBridge {
|
|
|
591
715
|
if (!appConfig) {
|
|
592
716
|
throw new Error(`Application ${id} is not found in app config`);
|
|
593
717
|
}
|
|
718
|
+
const manifestPromise = import_loaders.ManifestLoader.get(appConfig);
|
|
719
|
+
manifestPromise.catch(() => {
|
|
720
|
+
});
|
|
594
721
|
const frameEle = await import_frame.Frame.create({
|
|
595
722
|
id,
|
|
596
723
|
instanceId,
|
|
597
724
|
manifestPath: appConfig.manifestPath,
|
|
598
725
|
hostUrl: appConfig.hostUrl,
|
|
599
|
-
options: {
|
|
600
|
-
title: appConfig.name,
|
|
601
|
-
...frameOptions
|
|
602
|
-
}
|
|
726
|
+
options: { title: appConfig.name, ...frameOptions }
|
|
603
727
|
});
|
|
604
728
|
if (!frameEle?.contentDocument)
|
|
605
729
|
throw new Error("unable to create iframe for the microapp");
|
|
@@ -609,6 +733,15 @@ class CAppBridge {
|
|
|
609
733
|
instanceId,
|
|
610
734
|
documentEle: frameEle.contentDocument
|
|
611
735
|
});
|
|
736
|
+
const microFEHost = this.#setupIframe(
|
|
737
|
+
frameEle.contentDocument,
|
|
738
|
+
appConfig,
|
|
739
|
+
{
|
|
740
|
+
id,
|
|
741
|
+
containerId: frameOptions?.containerId
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
const manifest = await manifestPromise;
|
|
612
745
|
await this.#loadApp({
|
|
613
746
|
instanceId,
|
|
614
747
|
history,
|
|
@@ -617,10 +750,16 @@ class CAppBridge {
|
|
|
617
750
|
containerId: frameOptions?.containerId,
|
|
618
751
|
...appConfig,
|
|
619
752
|
homeRoute: homeRoute ?? appConfig.homeRoute,
|
|
620
|
-
initialRoute
|
|
753
|
+
initialRoute,
|
|
754
|
+
microFEHost,
|
|
755
|
+
selfInitialize,
|
|
756
|
+
manifest
|
|
621
757
|
});
|
|
622
|
-
|
|
758
|
+
if (!selfInitialize) {
|
|
759
|
+
await this.#mountApp({ instanceId, ...appConfig });
|
|
760
|
+
}
|
|
623
761
|
this.#manageSession({ id, instanceId });
|
|
762
|
+
this.#bindModuleUnload(id, instanceId);
|
|
624
763
|
return instanceId;
|
|
625
764
|
} catch (err) {
|
|
626
765
|
this.#unloadApp({
|
|
@@ -654,6 +793,18 @@ class CAppBridge {
|
|
|
654
793
|
removeScriptingObject = (objectId, guestId) => {
|
|
655
794
|
this.#soManager.removeScriptingObject(objectId, guestId);
|
|
656
795
|
};
|
|
796
|
+
/**
|
|
797
|
+
* Pre-fetch the manifest for a guest application so that a subsequent
|
|
798
|
+
* {@link openApp} call can skip the manifest network round-trip.
|
|
799
|
+
* Safe to call multiple times — results are cached.
|
|
800
|
+
* @param id application id as defined in app.config.json
|
|
801
|
+
*/
|
|
802
|
+
warmUp = (id) => {
|
|
803
|
+
const appConfig = this.#microFEConfig.getConfigById(id);
|
|
804
|
+
if (!appConfig) return;
|
|
805
|
+
import_loaders.ManifestLoader.get(appConfig).catch(() => {
|
|
806
|
+
});
|
|
807
|
+
};
|
|
657
808
|
/**
|
|
658
809
|
* Unmount guest micro frontend application from DOM
|
|
659
810
|
* @param instanceId unique instance id of guest micro frontend application
|
package/dist/cjs/config/app.js
CHANGED
|
@@ -85,6 +85,14 @@ class CAppConfig {
|
|
|
85
85
|
* @returns true if key exists
|
|
86
86
|
*/
|
|
87
87
|
has = (key = "") => (0, import_has.default)(this.#gAppConfig, key);
|
|
88
|
+
/**
|
|
89
|
+
* Accept an already-parsed config object, skipping the HTTP fetch.
|
|
90
|
+
* Useful when the caller (e.g. pui-app-sdk) has already loaded the config.
|
|
91
|
+
* @param config
|
|
92
|
+
*/
|
|
93
|
+
setPreloadedConfig = (config) => {
|
|
94
|
+
this.#gAppConfig = config;
|
|
95
|
+
};
|
|
88
96
|
/**
|
|
89
97
|
* add version to the base url
|
|
90
98
|
* @returns versioned base url
|
package/dist/cjs/frame.html
CHANGED
|
@@ -6,27 +6,6 @@
|
|
|
6
6
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
7
7
|
<link rel="icon" href="/favicon.ico" />
|
|
8
8
|
<title>Application</title>
|
|
9
|
-
<script nonce="__CSP_NONCE__">
|
|
10
|
-
(function (i, s, o, g, r, a, m) {
|
|
11
|
-
i['GoogleAnalyticsObject'] = r;
|
|
12
|
-
(i[r] =
|
|
13
|
-
i[r] ||
|
|
14
|
-
function () {
|
|
15
|
-
(i[r].q = i[r].q || []).push(arguments);
|
|
16
|
-
}),
|
|
17
|
-
(i[r].l = 1 * new Date());
|
|
18
|
-
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
|
|
19
|
-
a.async = 1;
|
|
20
|
-
a.src = g;
|
|
21
|
-
m.parentNode.insertBefore(a, m);
|
|
22
|
-
})(
|
|
23
|
-
window,
|
|
24
|
-
document,
|
|
25
|
-
'script',
|
|
26
|
-
'https://www.google-analytics.com/analytics.js',
|
|
27
|
-
'ga',
|
|
28
|
-
);
|
|
29
|
-
</script>
|
|
30
9
|
<style nonce="__CSP_NONCE__">
|
|
31
10
|
.full-width {
|
|
32
11
|
width: 100%;
|
|
@@ -121,9 +121,11 @@ class ScriptLoader {
|
|
|
121
121
|
* This method:
|
|
122
122
|
* - Partitions assets into CDN (HTTP/HTTPS) and non-CDN (relative) arrays in a single pass
|
|
123
123
|
* - Creates modulepreload links for all assets for performance optimization
|
|
124
|
-
* -
|
|
125
|
-
*
|
|
126
|
-
* -
|
|
124
|
+
* - Appends all script elements to the DOM synchronously so the browser
|
|
125
|
+
* can download them in parallel
|
|
126
|
+
* - Execution order is preserved by the spec: type="module" scripts
|
|
127
|
+
* execute in document order; dynamic async=false scripts execute in
|
|
128
|
+
* insertion order
|
|
127
129
|
* @example
|
|
128
130
|
* ```typescript
|
|
129
131
|
* const scriptIds = await scriptLoader.load(
|
|
@@ -158,15 +160,13 @@ class ScriptLoader {
|
|
|
158
160
|
nonCdnAssets: []
|
|
159
161
|
}
|
|
160
162
|
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
href,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
return [...cdnAssets, ...nonCdnAssets].map((asset) => asset.id);
|
|
163
|
+
const orderedAssets = [...cdnAssets, ...nonCdnAssets];
|
|
164
|
+
await Promise.all(
|
|
165
|
+
orderedAssets.map(
|
|
166
|
+
({ id, href }) => this.#loadScript({ id, href, documentEle, isESMModule })
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
return orderedAssets.map((asset) => asset.id);
|
|
170
170
|
};
|
|
171
171
|
/**
|
|
172
172
|
* Removes a script element from the document by its ID.
|
|
@@ -155,14 +155,6 @@ const getServerHandlers = () => {
|
|
|
155
155
|
return versionedHandlers;
|
|
156
156
|
};
|
|
157
157
|
const serverHandlers = [
|
|
158
|
-
import_msw.rest.get(
|
|
159
|
-
"https://www.google-analytics.com/analytics.js",
|
|
160
|
-
(req, res, ctx) => res(
|
|
161
|
-
ctx.status(200),
|
|
162
|
-
ctx.set("Content-Type", "application/javascript"),
|
|
163
|
-
ctx.body("")
|
|
164
|
-
)
|
|
165
|
-
),
|
|
166
158
|
import_msw.rest.get(
|
|
167
159
|
"/latest/app.config.json",
|
|
168
160
|
(req, res, ctx) => res(ctx.json(import_app_config.default))
|