repluggable-core 1.302.66 → 1.302.67

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.
Files changed (303) hide show
  1. package/__tests__/spec-setup.js +3 -0
  2. package/dist/es/{API.d.ts → src/API.d.ts} +46 -8
  3. package/dist/es/src/API.d.ts.map +1 -0
  4. package/dist/es/src/API.js +3 -0
  5. package/dist/es/src/API.js.map +1 -0
  6. package/dist/es/src/IterableWeakMap.d.ts +20 -0
  7. package/dist/es/src/IterableWeakMap.d.ts.map +1 -0
  8. package/dist/es/src/IterableWeakMap.js +92 -0
  9. package/dist/es/src/IterableWeakMap.js.map +1 -0
  10. package/dist/es/src/__internal.d.ts.map +1 -0
  11. package/dist/es/src/__internal.js +5 -0
  12. package/dist/es/src/__internal.js.map +1 -0
  13. package/dist/es/src/appHost.d.ts +7 -0
  14. package/dist/es/src/appHost.d.ts.map +1 -0
  15. package/dist/es/src/appHost.js +898 -0
  16. package/dist/es/src/appHost.js.map +1 -0
  17. package/dist/es/src/appHostServices.d.ts +12 -0
  18. package/dist/es/src/appHostServices.d.ts.map +1 -0
  19. package/dist/es/src/appHostServices.js +25 -0
  20. package/dist/es/src/appHostServices.js.map +1 -0
  21. package/dist/es/src/appHostUtils.d.ts +4 -0
  22. package/dist/es/src/appHostUtils.d.ts.map +1 -0
  23. package/dist/es/src/appHostUtils.js +8 -0
  24. package/dist/es/src/appHostUtils.js.map +1 -0
  25. package/dist/es/src/extensionSlot.d.ts +8 -0
  26. package/dist/es/src/extensionSlot.d.ts.map +1 -0
  27. package/dist/es/src/extensionSlot.js +59 -0
  28. package/dist/es/src/extensionSlot.js.map +1 -0
  29. package/dist/es/src/hot.d.ts +14 -0
  30. package/dist/es/src/hot.d.ts.map +1 -0
  31. package/dist/es/src/hot.js +52 -0
  32. package/dist/es/src/hot.js.map +1 -0
  33. package/dist/es/src/index.d.ts +11 -0
  34. package/dist/es/src/index.d.ts.map +1 -0
  35. package/dist/es/src/index.js +10 -0
  36. package/dist/es/src/index.js.map +1 -0
  37. package/dist/es/src/installedShellsState.d.ts +19 -0
  38. package/dist/es/src/installedShellsState.d.ts.map +1 -0
  39. package/dist/es/src/installedShellsState.js +30 -0
  40. package/dist/es/src/installedShellsState.js.map +1 -0
  41. package/dist/es/src/interceptAnyObject.d.ts +7 -0
  42. package/dist/es/src/interceptAnyObject.d.ts.map +1 -0
  43. package/dist/es/src/interceptAnyObject.js +21 -0
  44. package/dist/es/src/interceptAnyObject.js.map +1 -0
  45. package/dist/es/src/invokeSlotCallbacks.d.ts +3 -0
  46. package/dist/es/src/invokeSlotCallbacks.d.ts.map +1 -0
  47. package/dist/es/src/invokeSlotCallbacks.js +20 -0
  48. package/dist/es/src/invokeSlotCallbacks.js.map +1 -0
  49. package/dist/es/src/loggers.d.ts +4 -0
  50. package/dist/es/src/loggers.d.ts.map +1 -0
  51. package/dist/es/src/loggers.js +100 -0
  52. package/dist/es/src/loggers.js.map +1 -0
  53. package/dist/es/src/monitorAPI.d.ts +3 -0
  54. package/dist/es/src/monitorAPI.d.ts.map +1 -0
  55. package/dist/es/src/monitorAPI.js +53 -0
  56. package/dist/es/src/monitorAPI.js.map +1 -0
  57. package/dist/es/src/repluggableAppDebug/index.d.ts +2 -0
  58. package/dist/es/src/repluggableAppDebug/index.d.ts.map +1 -0
  59. package/dist/es/src/repluggableAppDebug/index.js +2 -0
  60. package/dist/es/src/repluggableAppDebug/index.js.map +1 -0
  61. package/dist/es/src/repluggableAppDebug/performanceDebugInfo.d.ts +15 -0
  62. package/dist/es/src/repluggableAppDebug/performanceDebugInfo.d.ts.map +1 -0
  63. package/dist/es/src/repluggableAppDebug/performanceDebugInfo.js +94 -0
  64. package/dist/es/src/repluggableAppDebug/performanceDebugInfo.js.map +1 -0
  65. package/dist/es/src/repluggableAppDebug/repluggableAppDebug.d.ts +24 -0
  66. package/dist/es/src/repluggableAppDebug/repluggableAppDebug.d.ts.map +1 -0
  67. package/dist/es/src/repluggableAppDebug/repluggableAppDebug.js +132 -0
  68. package/dist/es/src/repluggableAppDebug/repluggableAppDebug.js.map +1 -0
  69. package/dist/es/src/tarjanGraph.d.ts +27 -0
  70. package/dist/es/src/tarjanGraph.d.ts.map +1 -0
  71. package/dist/es/src/tarjanGraph.js +94 -0
  72. package/dist/es/src/tarjanGraph.js.map +1 -0
  73. package/dist/es/src/throttledStore.d.ts +32 -0
  74. package/dist/es/src/throttledStore.d.ts.map +1 -0
  75. package/dist/es/src/throttledStore.js +240 -0
  76. package/dist/es/src/throttledStore.js.map +1 -0
  77. package/dist/es/test/IterableWeakMap.spec.d.ts +2 -0
  78. package/dist/es/test/IterableWeakMap.spec.d.ts.map +1 -0
  79. package/dist/es/test/IterableWeakMap.spec.js +218 -0
  80. package/dist/es/test/IterableWeakMap.spec.js.map +1 -0
  81. package/dist/es/test/appHost.mock.d.ts +4 -0
  82. package/dist/es/test/appHost.mock.d.ts.map +1 -0
  83. package/dist/es/test/appHost.mock.js +54 -0
  84. package/dist/es/test/appHost.mock.js.map +1 -0
  85. package/dist/es/test/appHost.spec.d.ts +2 -0
  86. package/dist/es/test/appHost.spec.d.ts.map +1 -0
  87. package/dist/es/test/appHost.spec.js +1451 -0
  88. package/dist/es/test/appHost.spec.js.map +1 -0
  89. package/dist/es/test/installedShellsState.spec.d.ts +2 -0
  90. package/dist/es/test/installedShellsState.spec.d.ts.map +1 -0
  91. package/dist/es/test/installedShellsState.spec.js +28 -0
  92. package/dist/es/test/installedShellsState.spec.js.map +1 -0
  93. package/dist/es/test/interceptAnyObject.spec.d.ts +2 -0
  94. package/dist/es/test/interceptAnyObject.spec.d.ts.map +1 -0
  95. package/dist/es/test/interceptAnyObject.spec.js +166 -0
  96. package/dist/es/test/interceptAnyObject.spec.js.map +1 -0
  97. package/dist/es/test/loggers.spec.d.ts +2 -0
  98. package/dist/es/test/loggers.spec.d.ts.map +1 -0
  99. package/dist/es/test/loggers.spec.js +218 -0
  100. package/dist/es/test/loggers.spec.js.map +1 -0
  101. package/dist/es/test/repluggableAppDebug.spec.d.ts +2 -0
  102. package/dist/es/test/repluggableAppDebug.spec.d.ts.map +1 -0
  103. package/dist/es/test/repluggableAppDebug.spec.js +107 -0
  104. package/dist/es/test/repluggableAppDebug.spec.js.map +1 -0
  105. package/dist/es/test/testKit.spec.d.ts +2 -0
  106. package/dist/es/test/testKit.spec.d.ts.map +1 -0
  107. package/dist/es/test/testKit.spec.js +129 -0
  108. package/dist/es/test/testKit.spec.js.map +1 -0
  109. package/dist/es/testKit/emptyLoggerOptions.d.ts +3 -0
  110. package/dist/es/testKit/emptyLoggerOptions.d.ts.map +1 -0
  111. package/dist/es/testKit/emptyLoggerOptions.js +15 -0
  112. package/dist/es/testKit/emptyLoggerOptions.js.map +1 -0
  113. package/dist/es/testKit/index.d.ts +27 -0
  114. package/dist/es/testKit/index.d.ts.map +1 -0
  115. package/dist/es/testKit/index.js +132 -0
  116. package/dist/es/testKit/index.js.map +1 -0
  117. package/dist/es/testKit/mockPackage.d.ts +23 -0
  118. package/dist/es/testKit/mockPackage.d.ts.map +1 -0
  119. package/dist/es/testKit/mockPackage.js +95 -0
  120. package/dist/es/testKit/mockPackage.js.map +1 -0
  121. package/dist/es/testKit/withConsoleErrors.d.ts +2 -0
  122. package/dist/es/testKit/withConsoleErrors.d.ts.map +1 -0
  123. package/dist/es/testKit/withConsoleErrors.js +12 -0
  124. package/dist/es/testKit/withConsoleErrors.js.map +1 -0
  125. package/dist/es/testKit/withThrowOnError.d.ts +3 -0
  126. package/dist/es/testKit/withThrowOnError.d.ts.map +1 -0
  127. package/dist/es/testKit/withThrowOnError.js +27 -0
  128. package/dist/es/testKit/withThrowOnError.js.map +1 -0
  129. package/dist/src/API.d.ts +46 -8
  130. package/dist/src/API.d.ts.map +1 -1
  131. package/dist/src/API.js +4 -0
  132. package/dist/src/API.js.map +1 -1
  133. package/dist/src/IterableWeakMap.d.ts +20 -0
  134. package/dist/src/IterableWeakMap.d.ts.map +1 -0
  135. package/dist/src/IterableWeakMap.js +96 -0
  136. package/dist/src/IterableWeakMap.js.map +1 -0
  137. package/dist/src/__internal.d.ts.map +1 -1
  138. package/dist/src/__internal.js +1 -1
  139. package/dist/src/__internal.js.map +1 -1
  140. package/dist/src/appHost.d.ts +7 -0
  141. package/dist/src/appHost.d.ts.map +1 -0
  142. package/dist/src/appHost.js +906 -0
  143. package/dist/src/appHost.js.map +1 -0
  144. package/dist/src/appHostServices.d.ts +12 -0
  145. package/dist/src/appHostServices.d.ts.map +1 -0
  146. package/dist/src/appHostServices.js +29 -0
  147. package/dist/src/appHostServices.js.map +1 -0
  148. package/dist/src/appHostUtils.d.ts +4 -0
  149. package/dist/src/appHostUtils.d.ts.map +1 -0
  150. package/dist/src/appHostUtils.js +16 -0
  151. package/dist/src/appHostUtils.js.map +1 -0
  152. package/dist/src/extensionSlot.d.ts +8 -0
  153. package/dist/src/extensionSlot.d.ts.map +1 -0
  154. package/dist/src/extensionSlot.js +67 -0
  155. package/dist/src/extensionSlot.js.map +1 -0
  156. package/dist/src/hot.d.ts +14 -0
  157. package/dist/src/hot.d.ts.map +1 -0
  158. package/dist/src/hot.js +56 -0
  159. package/dist/src/hot.js.map +1 -0
  160. package/dist/src/index.d.ts +10 -2
  161. package/dist/src/index.d.ts.map +1 -1
  162. package/dist/src/index.js +22 -15
  163. package/dist/src/index.js.map +1 -1
  164. package/dist/src/installedShellsState.d.ts +19 -0
  165. package/dist/src/installedShellsState.d.ts.map +1 -0
  166. package/dist/src/installedShellsState.js +38 -0
  167. package/dist/src/installedShellsState.js.map +1 -0
  168. package/dist/src/interceptAnyObject.d.ts +7 -0
  169. package/dist/src/interceptAnyObject.d.ts.map +1 -0
  170. package/dist/src/interceptAnyObject.js +28 -0
  171. package/dist/src/interceptAnyObject.js.map +1 -0
  172. package/dist/src/invokeSlotCallbacks.d.ts +3 -0
  173. package/dist/src/invokeSlotCallbacks.d.ts.map +1 -0
  174. package/dist/src/invokeSlotCallbacks.js +24 -0
  175. package/dist/src/invokeSlotCallbacks.js.map +1 -0
  176. package/dist/src/loggers.d.ts +4 -0
  177. package/dist/src/loggers.d.ts.map +1 -0
  178. package/dist/src/loggers.js +104 -0
  179. package/dist/src/loggers.js.map +1 -0
  180. package/dist/src/monitorAPI.d.ts +3 -0
  181. package/dist/src/monitorAPI.d.ts.map +1 -0
  182. package/dist/src/monitorAPI.js +57 -0
  183. package/dist/src/monitorAPI.js.map +1 -0
  184. package/dist/src/repluggableAppDebug/index.d.ts +2 -0
  185. package/dist/src/repluggableAppDebug/index.d.ts.map +1 -0
  186. package/dist/src/repluggableAppDebug/index.js +6 -0
  187. package/dist/src/repluggableAppDebug/index.js.map +1 -0
  188. package/dist/src/repluggableAppDebug/performanceDebugInfo.d.ts +15 -0
  189. package/dist/src/repluggableAppDebug/performanceDebugInfo.d.ts.map +1 -0
  190. package/dist/src/repluggableAppDebug/performanceDebugInfo.js +101 -0
  191. package/dist/src/repluggableAppDebug/performanceDebugInfo.js.map +1 -0
  192. package/dist/src/repluggableAppDebug/repluggableAppDebug.d.ts +24 -0
  193. package/dist/src/repluggableAppDebug/repluggableAppDebug.d.ts.map +1 -0
  194. package/dist/src/repluggableAppDebug/repluggableAppDebug.js +139 -0
  195. package/dist/src/repluggableAppDebug/repluggableAppDebug.js.map +1 -0
  196. package/dist/src/tarjanGraph.d.ts +27 -0
  197. package/dist/src/tarjanGraph.d.ts.map +1 -0
  198. package/dist/src/tarjanGraph.js +100 -0
  199. package/dist/src/tarjanGraph.js.map +1 -0
  200. package/dist/src/throttledStore.d.ts +32 -0
  201. package/dist/src/throttledStore.d.ts.map +1 -0
  202. package/dist/src/throttledStore.js +249 -0
  203. package/dist/src/throttledStore.js.map +1 -0
  204. package/dist/test/IterableWeakMap.spec.d.ts +2 -0
  205. package/dist/test/IterableWeakMap.spec.d.ts.map +1 -0
  206. package/dist/test/IterableWeakMap.spec.js +220 -0
  207. package/dist/test/IterableWeakMap.spec.js.map +1 -0
  208. package/dist/test/appHost.mock.d.ts +4 -0
  209. package/dist/test/appHost.mock.d.ts.map +1 -0
  210. package/dist/test/appHost.mock.js +59 -0
  211. package/dist/test/appHost.mock.js.map +1 -0
  212. package/dist/test/appHost.spec.d.ts +2 -0
  213. package/dist/test/appHost.spec.d.ts.map +1 -0
  214. package/dist/test/appHost.spec.js +1456 -0
  215. package/dist/test/appHost.spec.js.map +1 -0
  216. package/dist/test/installedShellsState.spec.d.ts +2 -0
  217. package/dist/test/installedShellsState.spec.d.ts.map +1 -0
  218. package/dist/test/installedShellsState.spec.js +30 -0
  219. package/dist/test/installedShellsState.spec.js.map +1 -0
  220. package/dist/test/interceptAnyObject.spec.d.ts +2 -0
  221. package/dist/test/interceptAnyObject.spec.d.ts.map +1 -0
  222. package/dist/test/interceptAnyObject.spec.js +171 -0
  223. package/dist/test/interceptAnyObject.spec.js.map +1 -0
  224. package/dist/test/loggers.spec.d.ts +2 -0
  225. package/dist/test/loggers.spec.d.ts.map +1 -0
  226. package/dist/test/loggers.spec.js +220 -0
  227. package/dist/test/loggers.spec.js.map +1 -0
  228. package/dist/test/repluggableAppDebug.spec.d.ts +2 -0
  229. package/dist/test/repluggableAppDebug.spec.d.ts.map +1 -0
  230. package/dist/test/repluggableAppDebug.spec.js +109 -0
  231. package/dist/test/repluggableAppDebug.spec.js.map +1 -0
  232. package/dist/test/testKit.spec.d.ts +2 -0
  233. package/dist/test/testKit.spec.d.ts.map +1 -0
  234. package/dist/test/testKit.spec.js +134 -0
  235. package/dist/test/testKit.spec.js.map +1 -0
  236. package/dist/testKit/emptyLoggerOptions.d.ts +3 -0
  237. package/dist/testKit/emptyLoggerOptions.d.ts.map +1 -0
  238. package/dist/testKit/emptyLoggerOptions.js +18 -0
  239. package/dist/testKit/emptyLoggerOptions.js.map +1 -0
  240. package/dist/testKit/index.d.ts +27 -0
  241. package/dist/testKit/index.d.ts.map +1 -0
  242. package/dist/testKit/index.js +160 -0
  243. package/dist/testKit/index.js.map +1 -0
  244. package/dist/testKit/mockPackage.d.ts +23 -0
  245. package/dist/testKit/mockPackage.d.ts.map +1 -0
  246. package/dist/testKit/mockPackage.js +101 -0
  247. package/dist/testKit/mockPackage.js.map +1 -0
  248. package/dist/testKit/withConsoleErrors.d.ts +2 -0
  249. package/dist/testKit/withConsoleErrors.d.ts.map +1 -0
  250. package/dist/testKit/withConsoleErrors.js +16 -0
  251. package/dist/testKit/withConsoleErrors.js.map +1 -0
  252. package/dist/testKit/withThrowOnError.d.ts +3 -0
  253. package/dist/testKit/withThrowOnError.d.ts.map +1 -0
  254. package/dist/testKit/withThrowOnError.js +31 -0
  255. package/dist/testKit/withThrowOnError.js.map +1 -0
  256. package/jest.config.js +16 -0
  257. package/package.json +21 -3
  258. package/src/API.ts +47 -9
  259. package/src/IterableWeakMap.ts +123 -0
  260. package/src/__internal.ts +1 -1
  261. package/src/appHost.ts +1222 -0
  262. package/src/appHostServices.ts +42 -0
  263. package/src/appHostUtils.ts +11 -0
  264. package/src/extensionSlot.ts +96 -0
  265. package/src/hot.ts +53 -0
  266. package/src/index.ts +42 -2
  267. package/src/installedShellsState.ts +54 -0
  268. package/src/interceptAnyObject.ts +39 -0
  269. package/src/invokeSlotCallbacks.ts +20 -0
  270. package/src/loggers.ts +112 -0
  271. package/src/monitorAPI.ts +71 -0
  272. package/src/repluggableAppDebug/debug.d.ts +37 -0
  273. package/src/repluggableAppDebug/index.ts +1 -0
  274. package/src/repluggableAppDebug/performanceDebugInfo.ts +96 -0
  275. package/src/repluggableAppDebug/repluggableAppDebug.ts +192 -0
  276. package/src/tarjanGraph.ts +96 -0
  277. package/src/throttledStore.ts +355 -0
  278. package/test/IterableWeakMap.spec.ts +280 -0
  279. package/test/appHost.mock.ts +58 -0
  280. package/test/appHost.spec.ts +1765 -0
  281. package/test/installedShellsState.spec.ts +39 -0
  282. package/test/interceptAnyObject.spec.ts +217 -0
  283. package/test/loggers.spec.ts +288 -0
  284. package/test/repluggableAppDebug.spec.ts +118 -0
  285. package/test/testKit.spec.tsx +153 -0
  286. package/testKit/emptyLoggerOptions.ts +16 -0
  287. package/testKit/index.ts +170 -0
  288. package/testKit/mockPackage.ts +110 -0
  289. package/testKit/package.json +3 -0
  290. package/testKit/withConsoleErrors.ts +12 -0
  291. package/testKit/withThrowOnError.ts +34 -0
  292. package/tsconfig.json +9 -2
  293. package/dist/es/API.d.ts.map +0 -1
  294. package/dist/es/API.js +0 -2
  295. package/dist/es/API.js.map +0 -1
  296. package/dist/es/__internal.d.ts.map +0 -1
  297. package/dist/es/__internal.js +0 -5
  298. package/dist/es/__internal.js.map +0 -1
  299. package/dist/es/index.d.ts +0 -3
  300. package/dist/es/index.d.ts.map +0 -1
  301. package/dist/es/index.js +0 -2
  302. package/dist/es/index.js.map +0 -1
  303. /package/dist/es/{__internal.d.ts → src/__internal.d.ts} +0 -0
@@ -0,0 +1,1765 @@
1
+ import _ from 'lodash'
2
+
3
+ import { createAppHost, mainViewSlotKey, makeLazyEntryPoint, stateSlotKey } from '../src/appHost'
4
+
5
+ import {
6
+ AnySlotKey,
7
+ AppHost,
8
+ AppHostOptions,
9
+ EntryPoint,
10
+ HostLogger,
11
+ ObservableState,
12
+ PrivateAppHost,
13
+ PrivateShell,
14
+ Shell,
15
+ SlotKey
16
+ } from '../src/API'
17
+ import {
18
+ addMockShell,
19
+ asyncLoadMockPackage,
20
+ dependsOnMockPackageEntryPoint,
21
+ emptyLoggerOptions,
22
+ MockAPI,
23
+ mockPackage,
24
+ mockPackageWithPublicAPI,
25
+ mockPackageWithSlot,
26
+ MockPublicAPI,
27
+ mockShellInitialState,
28
+ mockShellStateKey,
29
+ MockSlot
30
+ } from '../testKit'
31
+
32
+ import { AppHostAPI, AppHostServicesEntryPointName, AppHostServicesProvider } from '../src/appHostServices'
33
+ import { ConsoleHostLogger } from '../src/loggers'
34
+ import { createCircularEntryPoints, createDirectCircularEntryPoints } from './appHost.mock'
35
+
36
+ const testHostOptions: AppHostOptions = {
37
+ monitoring: { disableMonitoring: true }
38
+ }
39
+
40
+ const createHostWithDependantPackages = (DependencyAPI: AnySlotKey) => {
41
+ const MockAPI2: SlotKey<{}> = { name: 'Mock-API-2' }
42
+ const dependentPackage: EntryPoint[] = [
43
+ {
44
+ name: 'DEPENDENT_MOCK_ENTRY_POINT_1',
45
+ getDependencyAPIs() {
46
+ return [DependencyAPI]
47
+ }
48
+ },
49
+ {
50
+ name: 'DEPENDENT_MOCK_ENTRY_POINT_2',
51
+ getDependencyAPIs() {
52
+ return [DependencyAPI]
53
+ },
54
+ declareAPIs() {
55
+ return [MockAPI2]
56
+ },
57
+ attach(shell: Shell) {
58
+ shell.contributeAPI(MockAPI2, () => ({}))
59
+ }
60
+ }
61
+ ]
62
+
63
+ const deeplyDependentPackage: EntryPoint[] = [
64
+ {
65
+ name: 'DEPENDENT_MOCK_ENTRY_POINT_3',
66
+ getDependencyAPIs() {
67
+ return [MockAPI2]
68
+ }
69
+ }
70
+ ]
71
+
72
+ let getHelperShell: () => Shell = () => {
73
+ throw new Error()
74
+ }
75
+ const helperEntryPoint: EntryPoint = {
76
+ name: 'TEST_HELPER',
77
+ declareAPIs() {
78
+ return [DependencyAPI]
79
+ },
80
+ attach(shell: Shell) {
81
+ getHelperShell = () => shell
82
+ }
83
+ }
84
+
85
+ return {
86
+ host: createAppHost([dependentPackage, deeplyDependentPackage, helperEntryPoint], testHostOptions),
87
+ dependentPackage,
88
+ deeplyDependentPackage,
89
+ helperShell: getHelperShell()
90
+ }
91
+ }
92
+
93
+ interface ObservableValueState {
94
+ stateValue: number
95
+ }
96
+ interface ObservableValueSelector {
97
+ getStateValue(): number
98
+ }
99
+
100
+ interface EntryPointStateSnapshot {
101
+ canUseStore: boolean
102
+ canUseAPIs: boolean
103
+ wasInitializationCompleted: boolean
104
+ }
105
+
106
+ describe('App Host', () => {
107
+ beforeEach(() => {
108
+ spyOn(ConsoleHostLogger, 'log')
109
+ })
110
+
111
+ it('should create an app host', () => {
112
+ const host = createAppHost([], testHostOptions)
113
+ expect(host).toBeInstanceOf(Object)
114
+ })
115
+
116
+ describe('AppHost Options', () => {
117
+ it('should use ConsoleHostLogger by default', () => {
118
+ const host = createAppHost([], testHostOptions)
119
+ expect(host.log).toBe(ConsoleHostLogger)
120
+ })
121
+ it('should use custom host logger if specified', () => {
122
+ const logger: HostLogger = {
123
+ log() {},
124
+ spanRoot() {
125
+ return {
126
+ end() {}
127
+ } as any
128
+ },
129
+ spanChild() {
130
+ return {
131
+ end() {}
132
+ } as any
133
+ }
134
+ }
135
+ const options: AppHostOptions = {
136
+ logger,
137
+ monitoring: {}
138
+ }
139
+
140
+ const host = createAppHost([], options)
141
+
142
+ expect(host.log).toBe(logger)
143
+ })
144
+ })
145
+
146
+ describe('Packages Installation', () => {
147
+ it('should NOT throw on circular dependency if check is disabled in host options', () => {
148
+ const circularPackages = createDirectCircularEntryPoints()
149
+ const hostOptionsWithDisabledCircularCheck: AppHostOptions = {
150
+ monitoring: {},
151
+ disableCheckCircularDependencies: true
152
+ }
153
+ expect(() => createAppHost(circularPackages, hostOptionsWithDisabledCircularCheck)).not.toThrow()
154
+ })
155
+
156
+ it('should throw on direct circular API dependency (private keys)', () => {
157
+ const circularPackages = createDirectCircularEntryPoints()
158
+ expect(() => createAppHost(circularPackages, testHostOptions)).toThrowError()
159
+ })
160
+
161
+ it('should throw on direct circular API dependency (public keys)', () => {
162
+ const circularPackages = createDirectCircularEntryPoints(true)
163
+ expect(() => createAppHost(circularPackages, testHostOptions)).toThrowError()
164
+ })
165
+ it('should throw on circular API dependency (private keys)', () => {
166
+ const circularPackages = createCircularEntryPoints()
167
+ expect(() => createAppHost(circularPackages, testHostOptions)).toThrowError()
168
+ })
169
+
170
+ it('should throw on circular API dependency (public keys)', () => {
171
+ const circularPackages = createCircularEntryPoints(true)
172
+ expect(() => createAppHost(circularPackages, testHostOptions)).toThrowError()
173
+ })
174
+
175
+ it('should throw when dynamically adding a shell with circular dependency', () => {
176
+ const circularPackages = createCircularEntryPoints(true)
177
+ const nonCircular = circularPackages.slice(0, 3)
178
+ const circularEP = _.last(circularPackages) as EntryPoint
179
+ const host = createAppHost(nonCircular, testHostOptions)
180
+
181
+ expect(() => host.addShells([circularEP])).toThrow()
182
+ })
183
+
184
+ it('should install initial packages', () => {
185
+ const host = createAppHost([mockPackage], testHostOptions)
186
+ expect(host.hasShell(mockPackage.name)).toBe(true)
187
+ })
188
+
189
+ it('should install packages after initial installations', async () => {
190
+ const host = createAppHost([], testHostOptions)
191
+ expect(host.hasShell(mockPackage.name)).toBe(false)
192
+
193
+ await host.addShells([mockPackage])
194
+
195
+ expect(host.hasShell(mockPackage.name)).toBe(true)
196
+ })
197
+
198
+ it('should uninstall shell', async () => {
199
+ const host = createAppHost([mockPackage], testHostOptions)
200
+
201
+ await host.removeShells([mockPackage.name])
202
+
203
+ expect(host.hasShell(mockPackage.name)).toBe(false)
204
+ })
205
+
206
+ it('should not install multiple shells with the same name', () => {
207
+ expect(() => createAppHost([mockPackage, _.pick(mockPackage, 'name')], testHostOptions)).toThrow()
208
+ })
209
+
210
+ it('should install lazy shells', () => {
211
+ const lazyEntryPoint = makeLazyEntryPoint(mockPackage.name, async () => mockPackage)
212
+ const host = createAppHost([lazyEntryPoint], testHostOptions)
213
+ expect(host.hasShell(lazyEntryPoint.name)).toBe(true)
214
+ })
215
+ })
216
+
217
+ describe('EntryPoint lifecycle state', () => {
218
+ const takeEntryPointStateSnapshot = (shell: Shell): EntryPointStateSnapshot => {
219
+ return {
220
+ canUseStore: shell.canUseStore(),
221
+ canUseAPIs: shell.canUseAPIs(),
222
+ wasInitializationCompleted: shell.wasInitializationCompleted()
223
+ }
224
+ }
225
+
226
+ it('should be incomplete during appHost initialization', () => {
227
+ let stateInAttach: EntryPointStateSnapshot | undefined
228
+ let stateInExtend: EntryPointStateSnapshot | undefined
229
+
230
+ const entryPoint: EntryPoint = {
231
+ name: 'TEST_EP',
232
+ attach(shell) {
233
+ stateInAttach = takeEntryPointStateSnapshot(shell)
234
+ },
235
+ extend(shell) {
236
+ stateInExtend = takeEntryPointStateSnapshot(shell)
237
+ }
238
+ }
239
+
240
+ createAppHost([entryPoint], testHostOptions)
241
+
242
+ expect(stateInAttach).toMatchObject({
243
+ wasInitializationCompleted: false,
244
+ canUseAPIs: false,
245
+ canUseStore: false
246
+ })
247
+ expect(stateInExtend).toMatchObject({
248
+ wasInitializationCompleted: false,
249
+ canUseAPIs: true,
250
+ canUseStore: true
251
+ })
252
+ })
253
+
254
+ it('should be complete after appHost initialization', () => {
255
+ let shell: Shell | undefined
256
+
257
+ const entryPoint: EntryPoint = {
258
+ name: 'TEST_EP',
259
+ attach(_shell) {
260
+ shell = _shell
261
+ }
262
+ }
263
+
264
+ createAppHost([entryPoint], testHostOptions)
265
+ const stateAfter = shell && takeEntryPointStateSnapshot(shell)
266
+
267
+ expect(stateAfter).toMatchObject({
268
+ wasInitializationCompleted: true,
269
+ canUseAPIs: true,
270
+ canUseStore: true
271
+ })
272
+ })
273
+
274
+ it('should be incomplete during lifecycle of added entry point', () => {
275
+ let stateInAttach: EntryPointStateSnapshot | undefined
276
+ let stateInExtend: EntryPointStateSnapshot | undefined
277
+
278
+ const host = createAppHost([], testHostOptions)
279
+ addMockShell(host, {
280
+ attach(shell) {
281
+ stateInAttach = takeEntryPointStateSnapshot(shell)
282
+ },
283
+ extend(shell) {
284
+ stateInExtend = takeEntryPointStateSnapshot(shell)
285
+ }
286
+ })
287
+
288
+ expect(stateInAttach).toMatchObject({
289
+ wasInitializationCompleted: false,
290
+ canUseAPIs: false,
291
+ canUseStore: false
292
+ })
293
+ expect(stateInExtend).toMatchObject({
294
+ wasInitializationCompleted: false,
295
+ canUseAPIs: true,
296
+ canUseStore: true
297
+ })
298
+ })
299
+
300
+ it('should be complete after lifecycle of added entry point', () => {
301
+ const host = createAppHost([], testHostOptions)
302
+
303
+ const shell = addMockShell(host)
304
+
305
+ const stateAfter = takeEntryPointStateSnapshot(shell)
306
+ expect(stateAfter).toMatchObject({
307
+ wasInitializationCompleted: true,
308
+ canUseAPIs: true,
309
+ canUseStore: true
310
+ })
311
+ })
312
+
313
+ it('should be incomplete during execution of late initializer', () => {
314
+ let state: EntryPointStateSnapshot | undefined
315
+
316
+ const host = createAppHost([], testHostOptions)
317
+ const shell = addMockShell(host)
318
+ expect(shell.wasInitializationCompleted()).toBe(true)
319
+
320
+ shell.runLateInitializer(() => {
321
+ state = takeEntryPointStateSnapshot(shell)
322
+ })
323
+
324
+ expect(state).toMatchObject({
325
+ canUseStore: true,
326
+ canUseAPIs: true,
327
+ wasInitializationCompleted: false
328
+ })
329
+ })
330
+
331
+ it('should be complete after execution of late initializer', () => {
332
+ const host = createAppHost([], testHostOptions)
333
+ const shell = addMockShell(host)
334
+ expect(shell.wasInitializationCompleted()).toBe(true)
335
+
336
+ shell.runLateInitializer(() => {})
337
+
338
+ const stateAfter = takeEntryPointStateSnapshot(shell)
339
+ expect(stateAfter).toMatchObject({
340
+ canUseStore: true,
341
+ canUseAPIs: true,
342
+ wasInitializationCompleted: true
343
+ })
344
+ })
345
+
346
+ it('should be complete after execution of late initializer that throws', () => {
347
+ const host = createAppHost([], testHostOptions)
348
+ const shell = addMockShell(host)
349
+ expect(shell.wasInitializationCompleted()).toBe(true)
350
+
351
+ expect(() => {
352
+ shell.runLateInitializer(() => {
353
+ throw new Error('TEST-ERROR')
354
+ })
355
+ }).toThrow('TEST-ERROR')
356
+
357
+ const stateAfter = takeEntryPointStateSnapshot(shell)
358
+ expect(stateAfter).toMatchObject({
359
+ canUseStore: true,
360
+ canUseAPIs: true,
361
+ wasInitializationCompleted: true
362
+ })
363
+ })
364
+ })
365
+
366
+ _.forEach(
367
+ [
368
+ {
369
+ testCase: 'private API keys',
370
+ dependencyAPI: MockAPI,
371
+ providerPackage: mockPackage
372
+ },
373
+ {
374
+ testCase: 'public API keys',
375
+ dependencyAPI: { name: MockPublicAPI.name, public: true },
376
+ providerPackage: mockPackageWithPublicAPI
377
+ }
378
+ ],
379
+ ({ testCase, dependencyAPI, providerPackage }) => {
380
+ describe(`Dependency entry point installation (${testCase})`, () => {
381
+ it('should not install dependent entry point until dependency is installed', async () => {
382
+ const { host, dependentPackage } = createHostWithDependantPackages(dependencyAPI)
383
+
384
+ expect(host.hasShell(dependentPackage[0].name)).toBe(false)
385
+
386
+ await host.addShells([providerPackage])
387
+
388
+ expect(host.hasShell(dependentPackage[0].name)).toBe(true)
389
+ })
390
+
391
+ it('should install all dependent entry points chain when dependencies are installed from entry point', async () => {
392
+ const { host, dependentPackage, deeplyDependentPackage } = createHostWithDependantPackages(dependencyAPI)
393
+
394
+ expect(host.hasShell(dependentPackage[0].name)).toBe(false)
395
+ expect(host.hasShell(dependentPackage[1].name)).toBe(false)
396
+ expect(host.hasShell(deeplyDependentPackage[0].name)).toBe(false)
397
+
398
+ await host.addShells([providerPackage])
399
+
400
+ expect(host.hasShell(dependentPackage[0].name)).toBe(true)
401
+ expect(host.hasShell(dependentPackage[1].name)).toBe(true)
402
+ expect(host.hasShell(deeplyDependentPackage[0].name)).toBe(true)
403
+ })
404
+
405
+ it('should install all dependent entry points chain when dependencies are installed outside of entry point', async () => {
406
+ const { host, dependentPackage, deeplyDependentPackage, helperShell } = createHostWithDependantPackages(dependencyAPI)
407
+
408
+ expect(host.hasShell(dependentPackage[0].name)).toBe(false)
409
+ expect(host.hasShell(dependentPackage[1].name)).toBe(false)
410
+ expect(host.hasShell(deeplyDependentPackage[0].name)).toBe(false)
411
+
412
+ helperShell.contributeAPI(dependencyAPI, () => ({
413
+ stubTrue: () => true,
414
+ getNewObject: () => ({})
415
+ }))
416
+
417
+ expect(host.hasShell(dependentPackage[0].name)).toBe(true)
418
+ expect(host.hasShell(dependentPackage[1].name)).toBe(true)
419
+ expect(host.hasShell(deeplyDependentPackage[0].name)).toBe(true)
420
+ })
421
+
422
+ it('should uninstall all dependent entry points chain when dependencies are uninstalled', async () => {
423
+ const { host, dependentPackage, deeplyDependentPackage } = createHostWithDependantPackages(dependencyAPI)
424
+
425
+ await host.addShells([providerPackage])
426
+ await host.removeShells([providerPackage.name])
427
+
428
+ expect(host.hasShell(dependentPackage[0].name)).toBe(false)
429
+ expect(host.hasShell(dependentPackage[1].name)).toBe(false)
430
+ expect(host.hasShell(deeplyDependentPackage[0].name)).toBe(false)
431
+ })
432
+ })
433
+ }
434
+ )
435
+
436
+ describe('Host extension slots', () => {
437
+ describe('hasSlot', () => {
438
+ it('should return false for hasSlot if the slot is not defined on the host', () => {
439
+ const host = createAppHost([], testHostOptions)
440
+ expect(host.hasSlot(MockSlot)).toBeFalsy()
441
+ })
442
+
443
+ it('should return true for hasSlot if the slot is defined on the host', () => {
444
+ const host = createAppHost([mockPackageWithSlot], testHostOptions)
445
+ expect(host.hasSlot(MockSlot)).toBeTruthy()
446
+ })
447
+ })
448
+ it('should have a state extension slot', () => {
449
+ const host = createAppHost([], testHostOptions)
450
+ expect(host.getSlot(stateSlotKey)).toBeTruthy()
451
+ })
452
+
453
+ it('should have a main view extension slot', () => {
454
+ const host = createAppHost([], testHostOptions)
455
+ expect(host.getSlot(mainViewSlotKey)).toBeTruthy()
456
+ })
457
+
458
+ it('should retrieve all slot keys', () => {
459
+ const sortSlotKeys = (slotKeys: AnySlotKey[]) => _.sortBy(slotKeys, 'name')
460
+
461
+ const host = createAppHost([mockPackage], testHostOptions)
462
+
463
+ const actual = sortSlotKeys(host.getAllSlotKeys())
464
+ const expected = sortSlotKeys([AppHostAPI, mainViewSlotKey, stateSlotKey, MockAPI])
465
+
466
+ expect(actual).toEqual(expected)
467
+ })
468
+
469
+ describe('private API slot key', () => {
470
+ it('should equal itself', () => {
471
+ const host = createAppHost([mockPackage], testHostOptions)
472
+
473
+ const API = host.getAPI(MockAPI)
474
+
475
+ expect(API).toBeTruthy()
476
+ })
477
+
478
+ it('should not equal another key with same name', () => {
479
+ const host = createAppHost([mockPackage], testHostOptions)
480
+ const fakeKey: SlotKey<MockAPI> = { name: MockAPI.name }
481
+
482
+ expect(() => {
483
+ host.getAPI(fakeKey)
484
+ }).toThrowError(new RegExp(MockAPI.name))
485
+ })
486
+
487
+ it('should not equal another key with same name that claims it is public', () => {
488
+ const host = createAppHost([mockPackage], testHostOptions)
489
+ const fakeKey1: SlotKey<MockAPI> = {
490
+ name: MockAPI.name,
491
+ public: true
492
+ }
493
+ const fakeKey2: SlotKey<MockAPI> = {
494
+ name: MockAPI.name,
495
+ public: false
496
+ }
497
+ const fakeKey3: any = {
498
+ name: MockAPI.name,
499
+ public: 'zzz'
500
+ }
501
+
502
+ expect(() => host.getAPI(fakeKey1)).toThrowError(new RegExp(MockAPI.name))
503
+ expect(() => host.getAPI(fakeKey2)).toThrowError(new RegExp(MockAPI.name))
504
+ expect(() => host.getAPI(fakeKey3)).toThrowError(new RegExp(MockAPI.name))
505
+ })
506
+ })
507
+
508
+ describe('public API slot key', () => {
509
+ it('should equal itself', () => {
510
+ const host = createAppHost([mockPackageWithPublicAPI], testHostOptions)
511
+
512
+ const API = host.getAPI(MockPublicAPI)
513
+
514
+ expect(API).toBeTruthy()
515
+ })
516
+
517
+ it('should equal another key with same name that claims it is public', () => {
518
+ const host = createAppHost([mockPackageWithPublicAPI], testHostOptions)
519
+ const anotherKey: SlotKey<MockAPI> = {
520
+ name: MockPublicAPI.name,
521
+ public: true
522
+ }
523
+
524
+ const API = host.getAPI(anotherKey)
525
+
526
+ expect(API).toBeTruthy()
527
+ })
528
+
529
+ it('should not equal another key with same name than does not claim it is public', () => {
530
+ const host = createAppHost([mockPackageWithPublicAPI], testHostOptions)
531
+ const anotherKey1: SlotKey<MockAPI> = {
532
+ name: MockPublicAPI.name
533
+ }
534
+ const anotherKey2: SlotKey<MockAPI> = {
535
+ name: MockPublicAPI.name,
536
+ public: false
537
+ }
538
+ const anotherKey3: any = {
539
+ name: MockPublicAPI.name,
540
+ public: 'zzz'
541
+ }
542
+
543
+ expect(() => host.getAPI(anotherKey1)).toThrowError(new RegExp(MockPublicAPI.name))
544
+ expect(() => host.getAPI(anotherKey2)).toThrowError(new RegExp(MockPublicAPI.name))
545
+ expect(() => host.getAPI(anotherKey3)).toThrowError(new RegExp(MockPublicAPI.name))
546
+ })
547
+ })
548
+ })
549
+
550
+ describe('Shell extension slots', () => {
551
+ describe('hasSlot', () => {
552
+ it('should return false for hasSlot if the slot is not defined on the host', async () => {
553
+ const host = createAppHost([], testHostOptions)
554
+ const entryPointWithoutSlot: EntryPoint = {
555
+ name: 'MOCK_WITHOUT_SLOT',
556
+ extend(shell) {
557
+ expect(shell.hasSlot(MockSlot)).toBeFalsy()
558
+ }
559
+ }
560
+ await host.addShells([entryPointWithoutSlot])
561
+ })
562
+
563
+ it('should return true for hasSlot if the slot is defined on the host', async () => {
564
+ const host = createAppHost([], testHostOptions)
565
+ const entryPointWithSlot: EntryPoint = {
566
+ name: 'MOCK_WITH_SLOT',
567
+ attach(shell) {
568
+ shell.declareSlot(MockSlot)
569
+ },
570
+ extend(shell) {
571
+ expect(shell.hasSlot(MockSlot)).toBeTruthy()
572
+ }
573
+ }
574
+ await host.addShells([entryPointWithSlot])
575
+ })
576
+ })
577
+ it('should allow contribution', async () => {
578
+ const host = createAppHost([], testHostOptions)
579
+ interface SlotItem {
580
+ value: string
581
+ }
582
+ interface MockAPIA {
583
+ contributeItem(fromShell: Shell, item: SlotItem): void
584
+ }
585
+ const slotKey: SlotKey<SlotItem> = {
586
+ name: 'MOCK_SLOT'
587
+ }
588
+ const MockAPIA: SlotKey<MockAPIA> = {
589
+ name: 'MOCK_API_A'
590
+ }
591
+ const contributedItemA = { value: 'A' }
592
+ const contributedItemB = { value: 'B' }
593
+ const entryPointA: EntryPoint = {
594
+ name: 'MOCK_A',
595
+ declareAPIs() {
596
+ return [MockAPIA]
597
+ },
598
+ attach(shell) {
599
+ shell.declareSlot(slotKey)
600
+ shell.contributeAPI(MockAPIA, () => ({
601
+ contributeItem(fromShell, item) {
602
+ shell.getSlot(slotKey).contribute(fromShell, item)
603
+ }
604
+ }))
605
+ },
606
+ extend(shell) {
607
+ shell.getAPI(MockAPIA).contributeItem(shell, contributedItemA)
608
+ }
609
+ }
610
+
611
+ const entryPointB: EntryPoint = {
612
+ name: 'MOCK_B',
613
+ getDependencyAPIs() {
614
+ return [MockAPIA]
615
+ },
616
+ extend(shell) {
617
+ shell.getAPI(MockAPIA).contributeItem(shell, contributedItemB)
618
+ }
619
+ }
620
+ await host.addShells([entryPointA, entryPointB])
621
+
622
+ const getItems = () =>
623
+ host
624
+ .getSlot(slotKey)
625
+ .getItems()
626
+ .map(({ contribution }) => contribution)
627
+
628
+ expect(getItems()).toEqual([contributedItemA, contributedItemB])
629
+
630
+ await host.removeShells(['MOCK_B'])
631
+ expect(getItems()).toEqual([contributedItemA])
632
+
633
+ await host.addShells([entryPointB])
634
+ expect(getItems()).toEqual([contributedItemA, contributedItemB])
635
+ })
636
+
637
+ it('should not allow direct access to slots from other shells', async () => {
638
+ const host = createAppHost([], testHostOptions)
639
+ interface SlotItem {
640
+ value: string
641
+ }
642
+ interface MockAPIA {
643
+ contributeItem(fromShell: Shell, item: SlotItem): void
644
+ }
645
+ const slotKey: SlotKey<SlotItem> = {
646
+ name: 'MOCK_SLOT'
647
+ }
648
+ const MockAPIA: SlotKey<MockAPIA> = {
649
+ name: 'MOCK_API_A'
650
+ }
651
+ const entryPointA: EntryPoint = {
652
+ name: 'MOCK_A',
653
+ declareAPIs() {
654
+ return [MockAPIA]
655
+ },
656
+ attach(shell) {
657
+ shell.declareSlot(slotKey)
658
+ }
659
+ }
660
+ const entryPointB: EntryPoint = {
661
+ name: 'MOCK_B',
662
+ extend(shell) {
663
+ const errorString = `Shell '${entryPointB.name}' is trying to get slot '${slotKey.name}' that is owned by '${entryPointA.name}'`
664
+ expect(() => shell.getSlot(slotKey)).toThrowError(errorString)
665
+ }
666
+ }
667
+ await host.addShells([entryPointA])
668
+ })
669
+ })
670
+
671
+ describe('Host State', () => {
672
+ it('should have a store with initial state', () => {
673
+ const host = createAppHost([], testHostOptions)
674
+ expect(host.getStore().getState()).toEqual({
675
+ $installedShells: {
676
+ installedShells: {
677
+ [AppHostServicesEntryPointName]: true
678
+ }
679
+ }
680
+ })
681
+ })
682
+ })
683
+
684
+ describe('Entry Point Contributions', () => {
685
+ it('should contribute API', () => {
686
+ const host = createAppHost([mockPackage], testHostOptions)
687
+ expect(host.getAPI(MockAPI)).toBeTruthy()
688
+ })
689
+
690
+ it('should contribute API after initial installations', async () => {
691
+ const host = createAppHost([], testHostOptions)
692
+ expect(() => host.getAPI(MockAPI)).toThrow()
693
+
694
+ await host.addShells([mockPackage])
695
+ expect(host.getAPI(MockAPI)).toBeTruthy()
696
+ })
697
+
698
+ it('should execute detach and attach sequence according to dependencies', async () => {
699
+ const MockAPI2: SlotKey<MockAPI> = { name: 'MOCK' }
700
+ const dependantEntryPoint: EntryPoint = {
701
+ name: 'EP1',
702
+ getDependencyAPIs: () => [MockAPI],
703
+ declareAPIs: () => [MockAPI2],
704
+ attach(shell) {
705
+ shell.contributeAPI(MockAPI2, () => shell.getAPI(MockAPI))
706
+ },
707
+ detach(shell) {
708
+ shell.getAPI(MockAPI).stubTrue()
709
+ }
710
+ }
711
+ const dependantEntryPoint2: EntryPoint = {
712
+ name: 'EP2',
713
+ getDependencyAPIs: () => [MockAPI, MockAPI2],
714
+ detach(shell) {
715
+ shell.getAPI(MockAPI).stubTrue()
716
+ shell.getAPI(MockAPI2).stubTrue()
717
+ }
718
+ }
719
+ const host = createAppHost([dependantEntryPoint2, mockPackage, dependantEntryPoint], testHostOptions)
720
+ expect(() => host.removeShells([mockPackage.name])).not.toThrow()
721
+ expect(host.hasShell(mockPackage.name)).toBe(false)
722
+ expect(host.hasShell(dependantEntryPoint.name)).toBe(false)
723
+ expect(host.hasShell(dependantEntryPoint2.name)).toBe(false)
724
+
725
+ await host.addShells([mockPackage])
726
+ expect(host.hasShell(dependantEntryPoint.name)).toBe(true)
727
+ expect(host.hasShell(dependantEntryPoint2.name)).toBe(true)
728
+ })
729
+
730
+ it('should contribute state', async () => {
731
+ const getMockShellState = (host: AppHost) => _.get(host.getStore().getState(), [mockPackage.name, mockShellStateKey], null)
732
+
733
+ const appHost = createAppHost([], testHostOptions)
734
+ expect(getMockShellState(appHost)).toBeNull()
735
+
736
+ await appHost.addShells([mockPackage])
737
+ expect(getMockShellState(appHost)).toEqual(mockShellInitialState)
738
+ })
739
+
740
+ describe('memoizeForState', () => {
741
+ interface MemoizedAPIInterface {
742
+ getNewObject(): object
743
+ }
744
+ const memoizedAPI: SlotKey<MemoizedAPIInterface> = {
745
+ name: 'memoizedAPI'
746
+ }
747
+
748
+ function contributeMemoizedAPI(host: AppHost, shouldClear?: () => boolean): Shell {
749
+ const createAPI = (shell: Shell): MemoizedAPIInterface => ({
750
+ getNewObject: shell.memoizeForState(() => ({}), _.stubTrue, shouldClear)
751
+ })
752
+ const mockShell = addMockShell(host, {
753
+ declareAPIs: () => [memoizedAPI],
754
+ attach(shell) {
755
+ shell.contributeAPI(memoizedAPI, () => createAPI(shell))
756
+ }
757
+ })
758
+
759
+ return mockShell
760
+ }
761
+
762
+ const createMockShell = (host: AppHost) => {
763
+ let observableState: ObservableState<ObservableValueSelector> = {} as any
764
+ const mockShell = addMockShell(host, {
765
+ declareAPIs: () => [memoizedAPI],
766
+ attach(shell) {
767
+ observableState = shell.contributeObservableState<ObservableValueState, ObservableValueSelector>(
768
+ () => ({
769
+ stateValue: (state = 1, action) => {
770
+ return action.type === 'increase' ? state + 1 : state
771
+ }
772
+ }),
773
+ state => {
774
+ return {
775
+ getStateValue: () => state.stateValue
776
+ }
777
+ }
778
+ )
779
+
780
+ shell.contributeAPI(memoizedAPI, () => ({
781
+ getNewObject: shell.memoizeForState(
782
+ () => ({
783
+ value: observableState.current(true).getStateValue()
784
+ }),
785
+ _.stubTrue
786
+ )
787
+ }))
788
+ }
789
+ })
790
+
791
+ return {
792
+ mockShell,
793
+ observableState
794
+ }
795
+ }
796
+
797
+ it('should memoize functions upon demand', () => {
798
+ const host = createAppHost([mockPackage], testHostOptions)
799
+ const getObj = () => host.getAPI(MockAPI).getNewObject()
800
+ expect(getObj()).not.toBe(getObj())
801
+
802
+ contributeMemoizedAPI(host)
803
+
804
+ const objForStateA = host.getAPI(memoizedAPI).getNewObject()
805
+ expect(objForStateA).toBe(host.getAPI(memoizedAPI).getNewObject())
806
+ })
807
+
808
+ it('should clear memoized functions on store dispatch', () => {
809
+ const host = createAppHost([mockPackage], testHostOptions)
810
+
811
+ contributeMemoizedAPI(host)
812
+
813
+ const objForStateA = host.getAPI(memoizedAPI).getNewObject()
814
+ expect(objForStateA).toBe(host.getAPI(memoizedAPI).getNewObject())
815
+
816
+ host.getStore().dispatch({ type: 'MOCK_ACTION' })
817
+ host.getStore().flush()
818
+
819
+ expect(objForStateA).not.toBe(host.getAPI(memoizedAPI).getNewObject())
820
+ })
821
+
822
+ it('should clear memoized functions on store dispatch when synchronous code is executing', () => {
823
+ const host = createAppHost([mockPackage], testHostOptions)
824
+
825
+ contributeMemoizedAPI(host)
826
+
827
+ const res1 = host.getAPI(memoizedAPI).getNewObject()
828
+ expect(res1).toBe(host.getAPI(memoizedAPI).getNewObject())
829
+
830
+ let res3
831
+ host.getStore().subscribe(() => {
832
+ // cache was flushing, so call memoized API in order to create new cache
833
+ const res2 = host.getAPI(memoizedAPI).getNewObject()
834
+ expect(res1).not.toBe(res2)
835
+ expect(res2).toBe(host.getAPI(memoizedAPI).getNewObject())
836
+ // dispatch new action for sync cache flushing
837
+ host.getStore().dispatch({ type: 'MOCK_ACTION' })
838
+
839
+ res3 = host.getAPI(memoizedAPI).getNewObject()
840
+ expect(res2).not.toBe(res3)
841
+ })
842
+
843
+ host.getStore().flush()
844
+ expect(res3).toBe(host.getAPI(memoizedAPI).getNewObject())
845
+ })
846
+
847
+ it('should clear memoized functions on observable dispatch', () => {
848
+ const host = createAppHost([mockPackage], testHostOptions)
849
+ const { mockShell } = createMockShell(host)
850
+
851
+ const objForStateA = host.getAPI(memoizedAPI).getNewObject()
852
+ expect(objForStateA).toBe(host.getAPI(memoizedAPI).getNewObject())
853
+
854
+ const { dispatch, flush } = mockShell.getStore<ObservableValueState>()
855
+ dispatch({ type: 'increase' })
856
+ flush()
857
+
858
+ expect(objForStateA).not.toBe(host.getAPI(memoizedAPI).getNewObject())
859
+ })
860
+
861
+ it('should clear memoized functions on observable dispatch when synchronous code is executing', () => {
862
+ const host = createAppHost([mockPackage], testHostOptions)
863
+ const { mockShell, observableState } = createMockShell(host)
864
+
865
+ const { dispatch, flush } = mockShell.getStore<ObservableValueState>()
866
+
867
+ const res1 = host.getAPI(memoizedAPI).getNewObject()
868
+ expect(res1).toBe(host.getAPI(memoizedAPI).getNewObject())
869
+
870
+ let res3
871
+ observableState.subscribe(mockShell, () => {
872
+ // cache was flushing, so call memoized API in order to create new cache
873
+ const res2 = host.getAPI(memoizedAPI).getNewObject()
874
+ expect(res1).not.toBe(res2)
875
+ expect(res2).toBe(host.getAPI(memoizedAPI).getNewObject())
876
+ // dispatch new action for sync cache flushing
877
+ dispatch({ type: 'increase' })
878
+
879
+ res3 = host.getAPI(memoizedAPI).getNewObject()
880
+ expect(res2).not.toBe(res3)
881
+ })
882
+
883
+ dispatch({ type: 'increase' })
884
+ flush()
885
+ expect(res3).toBe(host.getAPI(memoizedAPI).getNewObject())
886
+ })
887
+
888
+ it('should not clear memoized functions if not needed', () => {
889
+ const host = createAppHost([], testHostOptions)
890
+
891
+ contributeMemoizedAPI(host, _.stubFalse)
892
+
893
+ const objForStateA = host.getAPI(memoizedAPI).getNewObject()
894
+
895
+ host.getStore().dispatch({ type: 'MOCK_ACTION' })
896
+ host.getStore().flush()
897
+
898
+ expect(objForStateA).toBe(host.getAPI(memoizedAPI).getNewObject())
899
+ })
900
+ })
901
+
902
+ describe('memory cleanup:', () => {
903
+ let originalFinalizationRegistry: FinalizationRegistry<any>
904
+ let cleanupMemory = (ref: any) => {}
905
+
906
+ beforeEach(() => {
907
+ originalFinalizationRegistry = FinalizationRegistry as any
908
+ globalThis.FinalizationRegistry = function (cleanupCb: (heldValue: any) => void) {
909
+ const heldValueSet = new Map()
910
+
911
+ cleanupMemory = ref => {
912
+ const heldValue = heldValueSet.get(ref)
913
+
914
+ cleanupCb(heldValue)
915
+ }
916
+
917
+ return {
918
+ register(target: object, heldValue: any, unregisterToken?: object) {
919
+ heldValueSet.set(target, heldValue)
920
+ },
921
+ unregister() {}
922
+ } as unknown as FinalizationRegistry<any>
923
+ } as any
924
+ })
925
+
926
+ afterEach(() => {
927
+ globalThis.FinalizationRegistry = originalFinalizationRegistry as any
928
+ })
929
+
930
+ it('should remove memoized function from memory when there is no ref to memoized function', () => {
931
+ const host = createAppHost([], testHostOptions)
932
+
933
+ interface NewAPI {
934
+ getNewObject(): object
935
+ getNewObject2(): object
936
+ }
937
+ const newAPI: SlotKey<NewAPI> = { name: 'newAPI' }
938
+ let memFn1: _.MemoizedFunction | null = null
939
+ let memFn2: _.MemoizedFunction | null = null
940
+ const createAPI = (shell: Shell): NewAPI => {
941
+ memFn1 = shell.memoizeForState(() => ({}), _.stubTrue, _.stubTrue) as _.MemoizedFunction
942
+ memFn2 = shell.memoizeForState(() => ({}), _.stubTrue, _.stubTrue) as _.MemoizedFunction
943
+
944
+ memFn1.cache.clear = jest.fn()
945
+ memFn2.cache.clear = jest.fn()
946
+
947
+ return {
948
+ getNewObject: memFn1 as any,
949
+ getNewObject2: memFn2 as any
950
+ }
951
+ }
952
+ addMockShell(host, {
953
+ declareAPIs: () => [newAPI],
954
+ attach(shell) {
955
+ shell.contributeAPI(newAPI, () => createAPI(shell))
956
+ }
957
+ })
958
+
959
+ if (memFn1 && memFn2) {
960
+ const cacheFnMock1 = (memFn1 as any).cache.clear
961
+ const cacheFnMock2 = (memFn2 as any).cache.clear
962
+
963
+ host.getStore().dispatch({ type: 'MOCK_ACTION' })
964
+ host.getStore().flush()
965
+
966
+ expect(cacheFnMock1).toHaveBeenCalledTimes(1)
967
+ expect(cacheFnMock2).toHaveBeenCalledTimes(1)
968
+
969
+ cleanupMemory(memFn1)
970
+
971
+ host.getStore().dispatch({ type: 'MOCK_ACTION' })
972
+ host.getStore().flush()
973
+
974
+ expect(cacheFnMock1).toHaveBeenCalledTimes(1)
975
+ expect(cacheFnMock2).toHaveBeenCalledTimes(2)
976
+ } else {
977
+ throw Error('memFn1 or memFn2 is not defined')
978
+ }
979
+ })
980
+ })
981
+
982
+ describe('lazyEvaluator', () => {
983
+ it('should return a getter that is evaluated only once', () => {
984
+ const { helperShell } = createHostWithDependantPackages(MockAPI)
985
+ const func = jest.fn(() => 42)
986
+ const lazyEval = helperShell.lazyEvaluator(func)
987
+
988
+ expect(lazyEval.get()).toBe(42)
989
+ expect(lazyEval.get()).toBe(42)
990
+ expect(func).toHaveBeenCalledTimes(1)
991
+ })
992
+ })
993
+ })
994
+
995
+ describe('Entry Point Shell Scoping', () => {
996
+ describe('Shell hasAPI', () => {
997
+ it('should return false if the API is not defined on the host', async () => {
998
+ const host = createAppHost([], testHostOptions)
999
+ const entryPointWithoutAPI: EntryPoint = {
1000
+ name: 'ENTRY_POINT_WITHOUT_API',
1001
+ getDependencyAPIs: () => [],
1002
+ extend(shell: Shell) {
1003
+ expect(shell.hasAPI(MockAPI)).toBeFalsy()
1004
+ }
1005
+ }
1006
+ await host.addShells([entryPointWithoutAPI])
1007
+ })
1008
+ it('should return false if the API is defined on the host but is not declared as dependency', async () => {
1009
+ const host = createAppHost([mockPackage], testHostOptions)
1010
+ const entryPointWithoutAPIDependency: EntryPoint = {
1011
+ name: 'ENTRY_POINT_WITHOUT_API_DEPENDENCY',
1012
+ getDependencyAPIs: () => [],
1013
+ extend(shell: Shell) {
1014
+ expect(shell.hasAPI(MockAPI)).toBeFalsy()
1015
+ }
1016
+ }
1017
+ await host.addShells([entryPointWithoutAPIDependency])
1018
+ })
1019
+ it('should return true if the API is defined on the host and is declared as dependency', async () => {
1020
+ const host = createAppHost([mockPackage], testHostOptions)
1021
+ const entryPointWithAPIDependency: EntryPoint = {
1022
+ name: 'ENTRY_POINT_WITH_API_DEPENDENCY',
1023
+ getDependencyAPIs: () => [MockAPI],
1024
+ extend(shell: Shell) {
1025
+ expect(shell.hasAPI(MockAPI)).toBeTruthy()
1026
+ }
1027
+ }
1028
+ await host.addShells([entryPointWithAPIDependency])
1029
+ })
1030
+ it('should return true if the API is declared on the shell', async () => {
1031
+ const host = createAppHost([], testHostOptions)
1032
+ const entryPointWithAPIDeclaration: EntryPoint = {
1033
+ name: 'ENTRY_POINT_WITH_API_DECLARATION',
1034
+ declareAPIs: () => [MockAPI],
1035
+ attach(shell: Shell) {
1036
+ shell.contributeAPI(MockAPI, () => ({}))
1037
+ },
1038
+ extend(shell: Shell) {
1039
+ expect(shell.hasAPI(MockAPI)).toBeTruthy()
1040
+ }
1041
+ }
1042
+ await host.addShells([entryPointWithAPIDeclaration])
1043
+ })
1044
+ })
1045
+ it('should be able to call an API declared in dependencies', () => {
1046
+ const entryPointThatCallsAPI: EntryPoint = {
1047
+ name: 'ENTRY_POINT_WITH_API_CALL',
1048
+ getDependencyAPIs() {
1049
+ return [MockAPI]
1050
+ },
1051
+ extend(shell: Shell) {
1052
+ shell.getAPI(MockAPI).stubTrue()
1053
+ }
1054
+ }
1055
+ const appHost = createAppHost([mockPackage], testHostOptions)
1056
+ expect(() => appHost.addShells([entryPointThatCallsAPI])).not.toThrow()
1057
+ })
1058
+
1059
+ it('should not be able to call an API not declared in dependencies', () => {
1060
+ const entryPointThatCallsAPI: EntryPoint = {
1061
+ name: 'ENTRY_POINT_WITH_API_CALL',
1062
+ extend(shell: Shell) {
1063
+ shell.getAPI(MockAPI).stubTrue()
1064
+ }
1065
+ }
1066
+ const appHost = createAppHost([mockPackage], testHostOptions)
1067
+ expect(() => appHost.addShells([entryPointThatCallsAPI])).toThrow()
1068
+ })
1069
+
1070
+ it('should get scoped state', done => {
1071
+ const state = {}
1072
+ const MOCK_STATE_KEY = 'mockStateKey'
1073
+ const entryPointWithState: EntryPoint = {
1074
+ name: 'ENTRY_POINT_WITH_STATE',
1075
+ attach(shell: Shell) {
1076
+ shell.contributeState(() => ({
1077
+ [MOCK_STATE_KEY]: () => state
1078
+ }))
1079
+ },
1080
+ extend(shell: Shell) {
1081
+ expect(_.get(shell.getStore().getState(), MOCK_STATE_KEY)).toBe(state)
1082
+ done()
1083
+ }
1084
+ }
1085
+ createAppHost([entryPointWithState], testHostOptions)
1086
+ })
1087
+
1088
+ it('should be able to uninstall own installed packages', async () => {
1089
+ // note: this test assumes that addShells and removeShells complete synchronously
1090
+ const packageThatInstallsAPackage: EntryPoint = {
1091
+ name: 'ENTRY_POINT_THAT_INSTALLS_A_PACKAGE',
1092
+ extend(shell: Shell) {
1093
+ shell.addShells([mockPackage])
1094
+ shell.removeShells([mockPackage.name])
1095
+ }
1096
+ }
1097
+ createAppHost([packageThatInstallsAPackage], testHostOptions)
1098
+ expect(() => createAppHost([packageThatInstallsAPackage], testHostOptions)).not.toThrow()
1099
+ })
1100
+
1101
+ it('should not be able to uninstall not own installed packages', () => {
1102
+ const packageThatTriesToUninstallAPackage: EntryPoint = {
1103
+ name: 'ENTRY_POINT_THAT_TRYIES_TO_UNINSTALL_A_PACKAGE',
1104
+ extend(shell: Shell) {
1105
+ shell.removeShells([mockPackage.name])
1106
+ }
1107
+ }
1108
+ expect(() => createAppHost([mockPackage, packageThatTriesToUninstallAPackage], testHostOptions)).toThrow()
1109
+ })
1110
+
1111
+ it('should not trigger other reducers by scoped dispatch', () => {
1112
+ interface ScopedState {
1113
+ value: number
1114
+ }
1115
+ interface State1 {
1116
+ state1: ScopedState
1117
+ }
1118
+ const INITIAL_STATE: ScopedState = { value: 0 }
1119
+ const SET_VALUE = 'SET_VALUE'
1120
+ const setValue = (value: number) => ({ type: SET_VALUE, value })
1121
+
1122
+ const host = createAppHost([], {
1123
+ shouldScopeReducers: true,
1124
+ monitoring: {}
1125
+ })
1126
+ const shell1 = addMockShell(host, {
1127
+ attach(shell) {
1128
+ shell.contributeState<State1>(() => ({
1129
+ state1: (state: ScopedState = INITIAL_STATE, action): ScopedState => {
1130
+ switch (action.type) {
1131
+ case SET_VALUE:
1132
+ return { value: action.value }
1133
+ }
1134
+ return state
1135
+ }
1136
+ }))
1137
+ }
1138
+ })
1139
+
1140
+ // Initial state
1141
+ const getState = () => shell1.getStore<State1>().getState().state1.value
1142
+ expect(getState()).toBe(0)
1143
+
1144
+ // Scoped dispatch
1145
+ shell1.getStore<State1>().dispatch(setValue(73))
1146
+ expect(getState()).toBe(73)
1147
+
1148
+ // Unscoped dispatch
1149
+ host.getStore().dispatch(setValue(1337))
1150
+ expect(getState()).toBe(1337)
1151
+
1152
+ addMockShell(host, {
1153
+ extend(shell) {
1154
+ // Scoped dispatch in a different scope
1155
+ shell.getStore().dispatch(setValue(42))
1156
+ }
1157
+ })
1158
+ expect(getState()).toBe(1337)
1159
+ })
1160
+ })
1161
+
1162
+ describe('Entry Point HMR support', () => {
1163
+ const LowLevelSlotKey: SlotKey<string> = { name: 'LOW-LEVEL-SLOT' }
1164
+ const HighLevelSlotKey: SlotKey<string> = { name: 'HIGH-LEVEL-SLOT' }
1165
+ const ConsumerSlotKey: SlotKey<string> = { name: 'CONSUMER-SLOT' }
1166
+ const LowLevelAPI: SlotKey<{ lowLevelFunc(s: string): void }> = {
1167
+ name: 'LOW-LEVEL-API'
1168
+ }
1169
+ const HighLevelAPI: SlotKey<{ highLevelFunc(s: string): void }> = {
1170
+ name: 'HIGH-LEVEL-API'
1171
+ }
1172
+ const hmrTestPackage: EntryPoint[] = [
1173
+ {
1174
+ name: 'LOW_LEVEL_API_ENTRY_POINT',
1175
+ declareAPIs() {
1176
+ return [LowLevelAPI]
1177
+ },
1178
+ attach(shell: Shell) {
1179
+ shell.declareSlot(LowLevelSlotKey)
1180
+ shell.contributeAPI(LowLevelAPI, () => ({
1181
+ lowLevelFunc: jest.fn()
1182
+ }))
1183
+ }
1184
+ },
1185
+ {
1186
+ name: 'HIGH_LEVEL_API_ENTRY_POINT',
1187
+ getDependencyAPIs() {
1188
+ return [LowLevelAPI]
1189
+ },
1190
+ declareAPIs() {
1191
+ return [HighLevelAPI]
1192
+ },
1193
+ attach(shell: Shell) {
1194
+ shell.declareSlot(HighLevelSlotKey)
1195
+ shell.contributeAPI(HighLevelAPI, () => ({
1196
+ highLevelFunc: jest.fn
1197
+ }))
1198
+ },
1199
+ extend(shell: Shell) {
1200
+ shell.getAPI(LowLevelAPI).lowLevelFunc('HIGH')
1201
+ }
1202
+ },
1203
+ {
1204
+ name: 'CONSUMER_ENTRY_POINT',
1205
+ getDependencyAPIs() {
1206
+ return [HighLevelAPI]
1207
+ },
1208
+ extend(shell: Shell) {
1209
+ shell.declareSlot(ConsumerSlotKey)
1210
+ shell.getAPI(HighLevelAPI).highLevelFunc('CONSUMER')
1211
+ }
1212
+ }
1213
+ ]
1214
+
1215
+ it('should be able to reload entry points', async () => {
1216
+ const appHost = createAppHost(hmrTestPackage, testHostOptions)
1217
+
1218
+ await appHost.removeShells(['LOW_LEVEL_API_ENTRY_POINT'])
1219
+ await appHost.addShells([hmrTestPackage[0]])
1220
+
1221
+ expect(appHost.getAPI(HighLevelAPI)).toBeDefined()
1222
+ })
1223
+ })
1224
+
1225
+ describe('API layer', () => {
1226
+ it('should allow dependency from high to lower level API', async () => {
1227
+ const MockAPI1: SlotKey<{}> = { name: 'Mock-API', layer: 'INFRA' }
1228
+ const layers = [
1229
+ {
1230
+ level: 0,
1231
+ name: 'INFRA'
1232
+ },
1233
+ {
1234
+ level: 1,
1235
+ name: 'PRODUCT'
1236
+ }
1237
+ ]
1238
+
1239
+ const host = createAppHost([], { ...emptyLoggerOptions, layers })
1240
+ const EntryPoint1: EntryPoint = {
1241
+ name: 'MOCK_ENTRY_POINT_1',
1242
+ layer: 'PRODUCT',
1243
+ getDependencyAPIs: () => [MockAPI1]
1244
+ }
1245
+ const EntryPoint2: EntryPoint = {
1246
+ name: 'MOCK_ENTRY_POINT_2',
1247
+ layer: 'INFRA',
1248
+ declareAPIs: () => [MockAPI1],
1249
+ attach(shell) {
1250
+ shell.contributeAPI(MockAPI1, () => ({}))
1251
+ }
1252
+ }
1253
+ await host.addShells([EntryPoint2])
1254
+
1255
+ expect(() => host.addShells([EntryPoint1])).not.toThrow()
1256
+ })
1257
+
1258
+ it('should not allow dependency from low to higher level API', async () => {
1259
+ const MockAPI1: SlotKey<{}> = { name: 'Mock-API', layer: 'PRODUCT' }
1260
+ const layers = [
1261
+ {
1262
+ level: 0,
1263
+ name: 'INFRA'
1264
+ },
1265
+ {
1266
+ level: 1,
1267
+ name: 'PRODUCT'
1268
+ }
1269
+ ]
1270
+
1271
+ const host = createAppHost([], { ...emptyLoggerOptions, layers })
1272
+ const EntryPoint1: EntryPoint = {
1273
+ name: 'MOCK_ENTRY_POINT_1',
1274
+ layer: 'INFRA',
1275
+ getDependencyAPIs: () => [MockAPI1]
1276
+ }
1277
+ const EntryPoint2: EntryPoint = {
1278
+ name: 'MOCK_ENTRY_POINT_2',
1279
+ layer: 'PRODUCT',
1280
+ declareAPIs: () => [MockAPI1],
1281
+ attach(shell) {
1282
+ shell.contributeAPI(MockAPI1, () => ({}))
1283
+ }
1284
+ }
1285
+ await host.addShells([EntryPoint2])
1286
+
1287
+ expect(() => host.addShells([EntryPoint1])).toThrowError(
1288
+ `Entry point ${EntryPoint1.name} of layer ${layers[0].name} cannot depend on API ${MockAPI1.name} of layer ${EntryPoint2.layer}`
1289
+ )
1290
+ })
1291
+
1292
+ it('should not allow adding shell of unknown layer', () => {
1293
+ const MockAPI1: SlotKey<{}> = {
1294
+ name: 'Mock-API',
1295
+ layer: 'NON_EXIXTING_layer'
1296
+ }
1297
+ const layers = [
1298
+ {
1299
+ level: 0,
1300
+ name: 'INFRA'
1301
+ },
1302
+ {
1303
+ level: 1,
1304
+ name: 'PRODUCT'
1305
+ }
1306
+ ]
1307
+
1308
+ const host = createAppHost([], { ...emptyLoggerOptions, layers })
1309
+ const EntryPoint1: EntryPoint = {
1310
+ name: 'MOCK_ENTRY_POINT_1',
1311
+ layer: 'NON_EXIXTING_layer',
1312
+ getDependencyAPIs: () => [MockAPI1]
1313
+ }
1314
+ expect(() => host.addShells([EntryPoint1])).toThrowError(`Cannot find layer ${EntryPoint1.layer}`)
1315
+ })
1316
+
1317
+ it('should not allow contribution of API with non-matching entry point layer', () => {
1318
+ const MockAPI1: SlotKey<{}> = { name: 'Mock-API', layer: 'PRODUCT' }
1319
+ const layers = [
1320
+ {
1321
+ level: 0,
1322
+ name: 'INFRA'
1323
+ },
1324
+ {
1325
+ level: 1,
1326
+ name: 'PRODUCT'
1327
+ }
1328
+ ]
1329
+
1330
+ const host = createAppHost([], { ...emptyLoggerOptions, layers })
1331
+ const EntryPoint1: EntryPoint = {
1332
+ name: 'MOCK_ENTRY_POINT_1',
1333
+ layer: 'INFRA',
1334
+ declareAPIs: () => [MockAPI1],
1335
+ attach(shell) {
1336
+ shell.contributeAPI(MockAPI1, () => ({}))
1337
+ }
1338
+ }
1339
+ expect(() => host.addShells([EntryPoint1])).toThrowError(
1340
+ `Cannot contribute API ${MockAPI1.name} of layer ${MockAPI1.layer} from entry point ${EntryPoint1.name} of layer ${EntryPoint1.layer}`
1341
+ )
1342
+ })
1343
+
1344
+ it('should support multi dimensional layers definition', () => {
1345
+ const MockAPI1: SlotKey<{}> = {
1346
+ name: 'Mock-API',
1347
+ layer: ['COMMON', 'INFRA']
1348
+ }
1349
+ const layersDimension1 = [
1350
+ {
1351
+ level: 0,
1352
+ name: 'INFRA'
1353
+ },
1354
+ {
1355
+ level: 1,
1356
+ name: 'PRODUCT'
1357
+ }
1358
+ ]
1359
+ const layersDimension2 = [
1360
+ {
1361
+ level: 0,
1362
+ name: 'COMMON'
1363
+ },
1364
+ {
1365
+ level: 1,
1366
+ name: 'SPECIFIC'
1367
+ }
1368
+ ]
1369
+
1370
+ const host = createAppHost([], {
1371
+ ...emptyLoggerOptions,
1372
+ layers: [layersDimension1, layersDimension2]
1373
+ })
1374
+
1375
+ const EntryPoint1: EntryPoint = {
1376
+ name: 'MOCK_ENTRY_POINT_1',
1377
+ layer: ['INFRA', 'COMMON'],
1378
+ declareAPIs: () => [MockAPI1],
1379
+ attach(shell) {
1380
+ shell.contributeAPI(MockAPI1, () => ({}))
1381
+ }
1382
+ }
1383
+
1384
+ const EntryPoint2: EntryPoint = {
1385
+ name: 'MOCK_ENTRY_POINT_2',
1386
+ layer: ['PRODUCT', 'SPECIFIC'],
1387
+ getDependencyAPIs: () => [MockAPI1]
1388
+ }
1389
+
1390
+ expect(() => host.addShells([EntryPoint1, EntryPoint2])).not.toThrow()
1391
+ })
1392
+
1393
+ it('should throw for multi dimensional layers violation', () => {
1394
+ const MockAPI1: SlotKey<{}> = {
1395
+ name: 'Mock-API',
1396
+ layer: ['INFRA', 'SPECIFIC']
1397
+ }
1398
+ const layersDimension1 = [
1399
+ {
1400
+ level: 0,
1401
+ name: 'INFRA'
1402
+ },
1403
+ {
1404
+ level: 1,
1405
+ name: 'PRODUCT'
1406
+ }
1407
+ ]
1408
+ const layersDimension2 = [
1409
+ {
1410
+ level: 0,
1411
+ name: 'COMMON'
1412
+ },
1413
+ {
1414
+ level: 1,
1415
+ name: 'SPECIFIC'
1416
+ }
1417
+ ]
1418
+
1419
+ const host = createAppHost([], {
1420
+ ...emptyLoggerOptions,
1421
+ layers: [layersDimension1, layersDimension2]
1422
+ })
1423
+
1424
+ const EntryPoint1: EntryPoint = {
1425
+ name: 'MOCK_ENTRY_POINT_1',
1426
+ layer: ['INFRA', 'SPECIFIC'],
1427
+ declareAPIs: () => [MockAPI1],
1428
+ attach(shell) {
1429
+ shell.contributeAPI(MockAPI1, () => ({}))
1430
+ }
1431
+ }
1432
+
1433
+ const EntryPoint2: EntryPoint = {
1434
+ name: 'MOCK_ENTRY_POINT_2',
1435
+ layer: ['COMMON', 'PRODUCT'],
1436
+ getDependencyAPIs: () => [MockAPI1]
1437
+ }
1438
+
1439
+ expect(() => host.addShells([EntryPoint1, EntryPoint2])).toThrowError(
1440
+ `Entry point ${EntryPoint2.name} of layer COMMON cannot depend on API ${MockAPI1.name} of layer SPECIFIC`
1441
+ )
1442
+ })
1443
+
1444
+ it('should enforce cross-multi-dimensional-layers name uniqueness', () => {
1445
+ const layersDimension1 = [
1446
+ {
1447
+ level: 0,
1448
+ name: 'INFRA'
1449
+ },
1450
+ {
1451
+ level: 1,
1452
+ name: 'NOT_UNIQUE'
1453
+ }
1454
+ ]
1455
+ const layersDimension2 = [
1456
+ {
1457
+ level: 0,
1458
+ name: 'COMMON'
1459
+ },
1460
+ {
1461
+ level: 1,
1462
+ name: 'NOT_UNIQUE'
1463
+ }
1464
+ ]
1465
+
1466
+ expect(() =>
1467
+ createAppHost([], {
1468
+ ...emptyLoggerOptions,
1469
+ layers: [layersDimension1, layersDimension2]
1470
+ })
1471
+ ).toThrowError(`Cannot initialize host with non unique layers: NOT_UNIQUE`)
1472
+ })
1473
+
1474
+ it('should allow single layered API for multi dimensional layers host', () => {
1475
+ const MockAPI1: SlotKey<{}> = { name: 'Mock-API', layer: 'INFRA' }
1476
+ const layersDimension1 = [
1477
+ {
1478
+ level: 0,
1479
+ name: 'INFRA'
1480
+ },
1481
+ {
1482
+ level: 1,
1483
+ name: 'PRODUCT'
1484
+ }
1485
+ ]
1486
+ const layersDimension2 = [
1487
+ {
1488
+ level: 0,
1489
+ name: 'COMMON'
1490
+ },
1491
+ {
1492
+ level: 1,
1493
+ name: 'SPECIFIC'
1494
+ }
1495
+ ]
1496
+
1497
+ const EntryPoint1: EntryPoint = {
1498
+ name: 'MOCK_ENTRY_POINT_1',
1499
+ layer: 'PRODUCT',
1500
+ getDependencyAPIs: () => [MockAPI1]
1501
+ }
1502
+ const EntryPoint2: EntryPoint = {
1503
+ name: 'MOCK_ENTRY_POINT_2',
1504
+ layer: 'INFRA',
1505
+ declareAPIs: () => [MockAPI1],
1506
+ attach(shell) {
1507
+ shell.contributeAPI(MockAPI1, () => ({}))
1508
+ }
1509
+ }
1510
+
1511
+ expect(() =>
1512
+ createAppHost([EntryPoint1, EntryPoint2], {
1513
+ ...emptyLoggerOptions,
1514
+ layers: [layersDimension1, layersDimension2]
1515
+ })
1516
+ ).not.toThrow()
1517
+ })
1518
+ })
1519
+
1520
+ describe('API version', () => {
1521
+ it('should provide API of matching version', async () => {
1522
+ const MockAPIv0: SlotKey<{ f1(): void }> = { name: 'Mock-API' }
1523
+ const MockAPIv2: SlotKey<{ f2(): void }> = {
1524
+ name: 'Mock-API',
1525
+ version: 2
1526
+ }
1527
+ const host = createAppHost([])
1528
+ const entryPoint: EntryPoint = {
1529
+ name: 'MOCK_ENTRY_POINT',
1530
+ declareAPIs: () => [MockAPIv0, MockAPIv2],
1531
+ attach(shell) {
1532
+ shell.contributeAPI(MockAPIv0, () => ({ f1() {} }))
1533
+ shell.contributeAPI(MockAPIv2, () => ({ f2() {} }))
1534
+ }
1535
+ }
1536
+ await host.addShells([entryPoint])
1537
+
1538
+ expect(host.getAPI(MockAPIv0).f1).toBeDefined()
1539
+ expect((host.getAPI(MockAPIv0) as any).f2).not.toBeDefined()
1540
+
1541
+ expect(host.getAPI(MockAPIv2).f2).toBeDefined()
1542
+ expect((host.getAPI(MockAPIv2) as any).f1).not.toBeDefined()
1543
+
1544
+ const SecondMockAPIv2: SlotKey<{ f2(): void }> = {
1545
+ name: 'Mock-API',
1546
+ version: 2
1547
+ }
1548
+ expect(() => {
1549
+ addMockShell(host, {
1550
+ declareAPIs: () => [SecondMockAPIv2],
1551
+ attach(shell) {
1552
+ shell.contributeAPI(SecondMockAPIv2, () => ({ f2() {} }))
1553
+ }
1554
+ })
1555
+ }).toThrowError(
1556
+ new RegExp(`Extension slot with key '${SecondMockAPIv2.name}\\\(v${SecondMockAPIv2.version}\\\)' already exists`)
1557
+ )
1558
+ })
1559
+ })
1560
+
1561
+ describe('Host API', () => {
1562
+ describe('hasAPI', () => {
1563
+ it('should return false if the API is not defined on the host', () => {
1564
+ const host = createAppHost([], testHostOptions)
1565
+ expect(host.hasAPI(MockAPI)).toBeFalsy()
1566
+ })
1567
+
1568
+ it('should return true if the API is defined on the host', () => {
1569
+ const host = createAppHost([mockPackage], testHostOptions)
1570
+ expect(host.hasAPI(MockAPI)).toBeTruthy()
1571
+ })
1572
+ })
1573
+ it('should get all entry points', async () => {
1574
+ const host = createAppHost([mockPackage], testHostOptions) as AppHost & AppHostServicesProvider
1575
+ await host.addShells([mockPackageWithPublicAPI])
1576
+
1577
+ const allEntryPoints = host.getAPI(AppHostAPI).getAllEntryPoints()
1578
+
1579
+ expect(_.sortBy(allEntryPoints, 'name')).toEqual(
1580
+ _.sortBy([mockPackage, mockPackageWithPublicAPI, (host.getAppHostServicesShell() as PrivateShell).entryPoint], 'name')
1581
+ )
1582
+ })
1583
+
1584
+ it('should get host options', () => {
1585
+ const host = createAppHost([mockPackage], testHostOptions)
1586
+
1587
+ expect(host.getAPI(AppHostAPI).getAppHostOptions()).toEqual(testHostOptions)
1588
+ })
1589
+ })
1590
+
1591
+ describe('Cyclic Mode', () => {
1592
+ it('should load cyclic dependencies groups if all other dependencies are ready', () => {
1593
+ const API1: SlotKey<{}> = { name: 'API1' }
1594
+ const API2: SlotKey<{}> = { name: 'API2' }
1595
+ const API3: SlotKey<{}> = { name: 'API3' }
1596
+ const entryPoints: EntryPoint[] = [
1597
+ {
1598
+ name: 'Package1',
1599
+ getDependencyAPIs: () => [API2],
1600
+ declareAPIs: () => [API1],
1601
+ attach(shell) {
1602
+ shell.contributeAPI(API1, () => ({}))
1603
+ }
1604
+ },
1605
+ {
1606
+ name: 'Package2',
1607
+ getDependencyAPIs: () => [API3],
1608
+ declareAPIs: () => [API2],
1609
+ attach(shell) {
1610
+ shell.contributeAPI(API2, () => ({}))
1611
+ }
1612
+ },
1613
+ {
1614
+ name: 'Package3',
1615
+ getDependencyAPIs: () => [API1],
1616
+ declareAPIs: () => [API3],
1617
+ attach(shell) {
1618
+ shell.contributeAPI(API3, () => ({}))
1619
+ }
1620
+ }
1621
+ ]
1622
+ const host = createAppHost(entryPoints, {
1623
+ ...testHostOptions,
1624
+ experimentalCyclicMode: true
1625
+ })
1626
+
1627
+ expect(host.hasShell(entryPoints[0].name)).toBe(true)
1628
+ expect(host.hasShell(entryPoints[1].name)).toBe(true)
1629
+ expect(host.hasShell(entryPoints[2].name)).toBe(true)
1630
+ })
1631
+
1632
+ it('should not load cyclic dependencies groups if some other dependencies are not ready', () => {
1633
+ const API1: SlotKey<{}> = { name: 'API1' }
1634
+ const API2: SlotKey<{}> = { name: 'API2' }
1635
+ const API3: SlotKey<{}> = { name: 'API3' }
1636
+ const API4: SlotKey<{}> = { name: 'API4' }
1637
+ const entryPoints: EntryPoint[] = [
1638
+ {
1639
+ name: 'Package1',
1640
+ getDependencyAPIs: () => [API2],
1641
+ declareAPIs: () => [API1],
1642
+ attach(shell) {
1643
+ shell.contributeAPI(API1, () => ({}))
1644
+ }
1645
+ },
1646
+ {
1647
+ name: 'Package2',
1648
+ getDependencyAPIs: () => [API3],
1649
+ declareAPIs: () => [API2],
1650
+ attach(shell) {
1651
+ shell.contributeAPI(API2, () => ({}))
1652
+ }
1653
+ },
1654
+ {
1655
+ name: 'Package3',
1656
+ getDependencyAPIs: () => [API1, API4],
1657
+ declareAPIs: () => [API3],
1658
+ attach(shell) {
1659
+ shell.contributeAPI(API3, () => ({}))
1660
+ }
1661
+ }
1662
+ ]
1663
+ const host = createAppHost(entryPoints, {
1664
+ ...testHostOptions,
1665
+ experimentalCyclicMode: true
1666
+ })
1667
+
1668
+ expect(host.hasShell(entryPoints[0].name)).toBe(false)
1669
+ expect(host.hasShell(entryPoints[1].name)).toBe(false)
1670
+ expect(host.hasShell(entryPoints[2].name)).toBe(false)
1671
+ })
1672
+ })
1673
+
1674
+ describe('Host.executeWhenFree', () => {
1675
+ it('should be invoked immediately', () => {
1676
+ const host = createAppHost([], testHostOptions) as PrivateAppHost
1677
+ const spy = jest.fn()
1678
+ host.executeWhenFree('1', spy)
1679
+ expect(spy).toBeCalledTimes(1)
1680
+ })
1681
+
1682
+ it('when invoked during entryPoints installation, should run once installation resumes ', async () => {
1683
+ const host = createAppHost([], testHostOptions) as PrivateAppHost
1684
+ const spy = jest.fn()
1685
+
1686
+ const API1: SlotKey<{}> = { name: 'API1' }
1687
+ const API2: SlotKey<{}> = { name: 'API2' }
1688
+ const API3: SlotKey<{}> = { name: 'API3' }
1689
+
1690
+ const entryPoints: EntryPoint[] = [
1691
+ {
1692
+ name: 'Package1',
1693
+ declareAPIs: () => [API1],
1694
+ attach(shell) {
1695
+ host.executeWhenFree('1', spy)
1696
+ shell.contributeAPI(API1, () => ({}))
1697
+ },
1698
+ extend(shell: Shell) {
1699
+ expect(spy).toBeCalledTimes(0)
1700
+ }
1701
+ },
1702
+ {
1703
+ name: 'Package2',
1704
+ getDependencyAPIs: () => [API1],
1705
+ declareAPIs: () => [API2],
1706
+ attach(shell) {
1707
+ shell.contributeAPI(API2, () => ({}))
1708
+ },
1709
+ extend(shell: Shell) {
1710
+ expect(spy).toBeCalledTimes(0)
1711
+ }
1712
+ },
1713
+ {
1714
+ name: 'Package3',
1715
+ getDependencyAPIs: () => [API2],
1716
+ declareAPIs: () => [API3],
1717
+ attach(shell) {
1718
+ shell.contributeAPI(API3, () => ({}))
1719
+ },
1720
+ extend(shell: Shell) {
1721
+ expect(spy).toBeCalledTimes(0)
1722
+ }
1723
+ }
1724
+ ]
1725
+
1726
+ await host.addShells(entryPoints)
1727
+ expect(spy).toBeCalledTimes(1)
1728
+ })
1729
+ })
1730
+
1731
+ describe('Host.onDeclarationsChanged', () => {
1732
+ it('should be called once for an entry point attach phase', () => {
1733
+ const host = createAppHost([], testHostOptions)
1734
+ const spy = jest.fn()
1735
+ host.onDeclarationsChanged(spy)
1736
+ host.addShells([mockPackage, dependsOnMockPackageEntryPoint, mockPackageWithSlot])
1737
+ expect(spy).toBeCalledTimes(1)
1738
+ })
1739
+ it('should be called once for an entry point detach phase', () => {
1740
+ const host = createAppHost([], testHostOptions)
1741
+ const spy = jest.fn()
1742
+ host.onDeclarationsChanged(spy)
1743
+ host.addShells([mockPackage, dependsOnMockPackageEntryPoint, mockPackageWithSlot])
1744
+ expect(spy).toBeCalledTimes(1)
1745
+ host.removeShells([mockPackage.name])
1746
+ expect(spy).toBeCalledTimes(2)
1747
+ })
1748
+ it('should be called once for an async api contribution in the extend phase', () => {
1749
+ const host = createAppHost([], testHostOptions)
1750
+ const spy = jest.fn()
1751
+ host.onDeclarationsChanged(spy)
1752
+ host.addShells([asyncLoadMockPackage])
1753
+ expect(spy).toBeCalledTimes(1)
1754
+ })
1755
+ it('should be called for a slot declaration outside of an entry point', () => {
1756
+ const host = createAppHost([], testHostOptions)
1757
+ const spy = jest.fn()
1758
+ host.onDeclarationsChanged(spy)
1759
+ const shell = addMockShell(host)
1760
+ expect(spy).toBeCalledTimes(1)
1761
+ shell.declareSlot(MockSlot)
1762
+ expect(spy).toBeCalledTimes(2)
1763
+ })
1764
+ })
1765
+ })