@adobe/uix-host-react 0.10.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. package/dist/components/ExtensibleWrapper/ExtensibleWrapper.d.ts +33 -0
  2. package/dist/components/ExtensibleWrapper/ExtensibleWrapper.d.ts.map +1 -0
  3. package/dist/components/ExtensibleWrapper/ExtensionManagerProvider.d.ts +129 -0
  4. package/dist/components/ExtensibleWrapper/ExtensionManagerProvider.d.ts.map +1 -0
  5. package/dist/components/ExtensibleWrapper/ExtensionManagerProvider.test.d.ts +2 -0
  6. package/dist/components/ExtensibleWrapper/ExtensionManagerProvider.test.d.ts.map +1 -0
  7. package/dist/components/ExtensibleWrapper/UrlExtensionProvider.d.ts +37 -0
  8. package/dist/components/ExtensibleWrapper/UrlExtensionProvider.d.ts.map +1 -0
  9. package/dist/components/ExtensibleWrapper/UrlExtensionProvider.test.d.ts +2 -0
  10. package/dist/components/ExtensibleWrapper/UrlExtensionProvider.test.d.ts.map +1 -0
  11. package/dist/components/ExtensibleWrapper/index.d.ts +4 -0
  12. package/dist/components/ExtensibleWrapper/index.d.ts.map +1 -0
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/components/index.d.ts.map +1 -1
  15. package/dist/hooks/useExtensions.d.ts.map +1 -1
  16. package/dist/index.js +258 -3
  17. package/dist/index.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/components/ExtensibleWrapper/ExtensibleWrapper.tsx +133 -0
  20. package/src/components/ExtensibleWrapper/ExtensionManagerProvider.test.ts +214 -0
  21. package/src/components/ExtensibleWrapper/ExtensionManagerProvider.ts +324 -0
  22. package/src/components/ExtensibleWrapper/UrlExtensionProvider.test.ts +119 -0
  23. package/src/components/ExtensibleWrapper/UrlExtensionProvider.ts +93 -0
  24. package/src/components/ExtensibleWrapper/index.ts +15 -0
  25. package/src/components/index.ts +1 -0
  26. package/src/hooks/useExtensions.ts +9 -3
@@ -0,0 +1,324 @@
1
+ /*************************************************************************
2
+ * ADOBE CONFIDENTIAL
3
+ * ___________________
4
+ *
5
+ * Copyright 2024 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: All information contained herein is, and remains
9
+ * the property of Adobe and its suppliers, if any. The intellectual
10
+ * and technical concepts contained herein are proprietary to Adobe
11
+ * and its suppliers and are protected by all applicable intellectual
12
+ * property laws, including trade secret and copyright laws.
13
+ * Dissemination of this information or reproduction of this material
14
+ * is strictly forbidden unless prior written permission is obtained
15
+ * from Adobe.
16
+ **************************************************************************/
17
+
18
+ import {
19
+ createExtensionRegistryAsObjectsProvider,
20
+ ExtensionsProvider,
21
+ InstalledExtensions,
22
+ } from "@adobe/uix-host";
23
+
24
+ const EXTENSION_MANAGER_URL_PROD = "https://aemx-mngr.adobe.io";
25
+ const EXTENSION_MANAGER_URL_STAGE = "https://aemx-mngr-stage.adobe.io";
26
+
27
+ const APP_REGISTRY_URL_PROD = "https://appregistry.adobe.io";
28
+ const APP_REGISTRY_URL_STAGE = "https://appregistry-stage.adobe.io";
29
+
30
+ // Extension Manager stores information about extension points that a particular extension implements
31
+ // in the "extensionPoints" array of objects of the following "ExtensionPoint" type
32
+ // where "extensionPoint" is the name of the extension point, for example, "aem/assets/details/1"
33
+ // "url" is the extension url for the specified extension point
34
+ type ExtensionPoint = {
35
+ extensionPoint: string;
36
+ url: string;
37
+ };
38
+ export type ExtensionManagerExtension = {
39
+ id: string;
40
+ name: string;
41
+ title: string;
42
+ description: string;
43
+ status: string;
44
+ supportEmail: string;
45
+ extId: string;
46
+ disabled: boolean;
47
+ extensionPoints: ExtensionPoint[];
48
+ scope: Record<string, unknown>;
49
+ configuration?: Record<string, unknown>;
50
+ };
51
+
52
+ type AuthEMConfig = {
53
+ schema: "Bearer" | "Basic";
54
+ imsToken: string;
55
+ };
56
+ export interface ExtensionManagerConfig {
57
+ apiKey: string;
58
+ auth: AuthEMConfig;
59
+ service: string;
60
+ extensionPoint: string;
61
+ version: string;
62
+ imsOrg: string;
63
+ baseUrl: string;
64
+ scope?: Record<string, string>;
65
+ }
66
+
67
+ /** Authentication configuration, including IMS Org ID, access token, and API key */
68
+ export interface AuthConfig {
69
+ /** IMS Org ID */
70
+ imsOrg: string;
71
+ /** Access token for the user */
72
+ imsToken: string;
73
+ /** API key */
74
+ apiKey: string;
75
+ }
76
+
77
+ /** Discovery configuration, including environment and repo Id */
78
+ export interface DiscoveryConfig {
79
+ /** Environment level for backend Extension resolution services */
80
+ experienceShellEnvironment?: "prod" | "stage";
81
+ scope?: Record<string, string>;
82
+ }
83
+
84
+ /** Extension point ID */
85
+ export interface ExtensionPointId {
86
+ /** Service name */
87
+ service: string;
88
+ /** Extension point name */
89
+ name: string;
90
+ /** Extension point version */
91
+ version: string;
92
+ }
93
+
94
+ /**
95
+ * Sets up new ExtensionsProvider with authentication and discovery information needed to fetch the list of
96
+ * Extensions from AppRegistry and Extension Manager service, along with the query string portion of URL
97
+ * to extract the information about development Extensions
98
+ */
99
+ export interface ExtensionsProviderConfig {
100
+ /** Discovery configuration */
101
+ discoveryConfig: DiscoveryConfig;
102
+ /** Authentication configuration */
103
+ authConfig: AuthConfig;
104
+ /** Extension point ID */
105
+ extensionPointId: ExtensionPointId;
106
+ providerConfig: ExtensionProviderConfig;
107
+ }
108
+
109
+ export interface ExtensionProviderConfig {
110
+ extensionManagerUrl?: string;
111
+ appRegistryUrl?: string;
112
+ disableExtensionManager?: boolean;
113
+ }
114
+ export const getExtensionRegistryBaseUrl = (
115
+ environment: "prod" | "stage" | undefined,
116
+ registry: string | null
117
+ ): string =>
118
+ environment === "prod"
119
+ ? APP_REGISTRY_URL_PROD
120
+ : registry ?? APP_REGISTRY_URL_STAGE;
121
+
122
+ export const getExtensionManagerBaseUrl = (
123
+ environment: "prod" | "stage" | undefined,
124
+ extensionManager: string | null
125
+ ): string =>
126
+ environment === "prod"
127
+ ? EXTENSION_MANAGER_URL_PROD
128
+ : extensionManager ?? EXTENSION_MANAGER_URL_STAGE;
129
+
130
+ /**
131
+ * Extracts programId and envId from the repo value
132
+ * @param repo - the repo value
133
+ * @returns object with programId and envId
134
+ * @ignore
135
+ */
136
+ export function extractProgramIdEnvId(repo: string): {
137
+ programId: string;
138
+ envId: string;
139
+ } {
140
+ const regex: RegExp = /p(\d+)-e(\d+)/;
141
+ const match: RegExpMatchArray | null = regex.exec(repo);
142
+ if (!match) {
143
+ throw new Error("Error parsing a repo value");
144
+ }
145
+
146
+ return {
147
+ programId: match[1],
148
+ envId: match[2],
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Builds the URL for fetching extensions from the Extension Manager service
154
+ * @param config - the Extension Manager configuration
155
+ * @returns the URL for fetching extensions
156
+ * @ignore
157
+ */
158
+ export function buildExtensionManagerUrl(
159
+ config: ExtensionManagerConfig
160
+ ): string {
161
+ const scope = config.scope
162
+ ? Object.fromEntries(
163
+ Object.entries(config.scope).map(([k, v]) => [`scope.${k}`, v])
164
+ )
165
+ : {};
166
+ const extensionPoints: string = `${config.service}/${config.extensionPoint}/${config.version}`;
167
+ const queryParams = new URLSearchParams({
168
+ ...scope,
169
+ extensionPoints,
170
+ });
171
+
172
+ return `${config.baseUrl}/v2/extensions?${queryParams.toString()}`;
173
+ }
174
+
175
+ /**
176
+ * @ignore
177
+ */
178
+ export async function fetchExtensionsFromExtensionManager(
179
+ config: ExtensionManagerConfig
180
+ ): Promise<ExtensionManagerExtension[]> {
181
+ const resp: Response = await fetch(buildExtensionManagerUrl(config), {
182
+ headers: {
183
+ Authorization: `Bearer ${config.auth.imsToken}`,
184
+ "x-api-key": config.apiKey,
185
+ "x-org-id": config.imsOrg,
186
+ },
187
+ });
188
+
189
+ if (resp.status !== 200) {
190
+ throw new Error(
191
+ `Extension Manager returned non-200 response (${
192
+ resp.status
193
+ }): ${await resp.text()}`
194
+ );
195
+ }
196
+
197
+ return resp.json();
198
+ }
199
+
200
+ /**
201
+ * Takes an array of extensions from the App Registry, an array of extensions from the Extension Manager, and
202
+ * merges them into a list of Extensions. If an extension is disabled in the Extension Manager, it is removed from
203
+ * the list.
204
+ * Extension list from the App Registry is used as a base.
205
+ * @ignore
206
+ */
207
+ export function mergeExtensions(
208
+ appRegistryExtensions: InstalledExtensions,
209
+ extensionManagerExtensions: ExtensionManagerExtension[],
210
+ extensionPointId: ExtensionPointId
211
+ ): InstalledExtensions {
212
+ const mergedExtensions: InstalledExtensions = Object.assign(
213
+ appRegistryExtensions,
214
+ {}
215
+ );
216
+ extensionManagerExtensions.forEach((extension: ExtensionManagerExtension) => {
217
+ if (extension.disabled) {
218
+ // remove disabled extensions
219
+ delete mergedExtensions[extension.name];
220
+ } else {
221
+ const extPoint: ExtensionPoint | undefined =
222
+ extension.extensionPoints.find(
223
+ (_extensionPoint: ExtensionPoint) =>
224
+ _extensionPoint.extensionPoint ===
225
+ `${extensionPointId.service}/${extensionPointId.name}/${extensionPointId.version}`
226
+ );
227
+ if (extPoint) {
228
+ // add a new extension record or replace the existing one by an extension record from Extension Manager
229
+ // extension points are useful for filtering out extensions
230
+ mergedExtensions[extension.name] = {
231
+ id: extension.name,
232
+ url: extPoint.url,
233
+ configuration: extension.configuration,
234
+ extensionPoints: extension.extensionPoints.map(
235
+ (point) => point.extensionPoint
236
+ ),
237
+ };
238
+ } else {
239
+ //this should never happen because we query Extension Manager service for our specific extension point
240
+ console.warn(
241
+ `Extension point ${extensionPointId.service}/${extensionPointId.name}/${extensionPointId.version} not found for extension ${extension.name}`
242
+ );
243
+ }
244
+ }
245
+ });
246
+
247
+ return mergedExtensions;
248
+ }
249
+
250
+ async function getExtensionManagerExtensions(
251
+ discoveryConfig: DiscoveryConfig,
252
+ authConfig: AuthConfig,
253
+ providerConfig: ExtensionProviderConfig,
254
+ extensionPointId: ExtensionPointId
255
+ ): Promise<InstalledExtensions> {
256
+ const config = {
257
+ apiKey: authConfig.apiKey,
258
+ auth: {
259
+ schema: "Bearer",
260
+ imsToken: authConfig.imsToken,
261
+ },
262
+ service: extensionPointId.service,
263
+ extensionPoint: extensionPointId.name,
264
+ version: extensionPointId.version,
265
+ imsOrg: authConfig.imsOrg,
266
+ scope: discoveryConfig.scope,
267
+ };
268
+
269
+ const appRegistryConfig = {
270
+ ...config,
271
+ baseUrl: getExtensionRegistryBaseUrl(
272
+ discoveryConfig.experienceShellEnvironment,
273
+ providerConfig.appRegistryUrl
274
+ ),
275
+ } as ExtensionManagerConfig;
276
+ const appRegistryExtensionsProvider: ExtensionsProvider =
277
+ createExtensionRegistryAsObjectsProvider(appRegistryConfig);
278
+
279
+ const extensionManagerConfiguration = {
280
+ ...config,
281
+ baseUrl: getExtensionManagerBaseUrl(
282
+ discoveryConfig.experienceShellEnvironment,
283
+ providerConfig.extensionManagerUrl
284
+ ),
285
+ } as ExtensionManagerConfig;
286
+ const [appRegistryExtensions, extensionManagerExtensions] = await Promise.all(
287
+ [
288
+ appRegistryExtensionsProvider(),
289
+ providerConfig.disableExtensionManager
290
+ ? []
291
+ : fetchExtensionsFromExtensionManager(extensionManagerConfiguration),
292
+ ]
293
+ );
294
+
295
+ if (providerConfig.disableExtensionManager) {
296
+ return appRegistryExtensions;
297
+ } else {
298
+ return mergeExtensions(
299
+ appRegistryExtensions,
300
+ extensionManagerExtensions,
301
+ extensionPointId
302
+ );
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Creates an extension manager extension provider
308
+ * @ignore
309
+ */
310
+ export function createExtensionManagerExtensionsProvider(
311
+ discoveryConfig: DiscoveryConfig,
312
+ authConfig: AuthConfig,
313
+ providerConfig: ExtensionProviderConfig,
314
+ extensionPointId: ExtensionPointId
315
+ ): ExtensionsProvider {
316
+ return () => {
317
+ return getExtensionManagerExtensions(
318
+ discoveryConfig,
319
+ authConfig,
320
+ providerConfig,
321
+ extensionPointId
322
+ );
323
+ };
324
+ }
@@ -0,0 +1,119 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {
14
+ extractExtUrlParams,
15
+ generateExtensionId,
16
+ createUrlExtensionsProvider,
17
+ } from "./UrlExtensionProvider";
18
+ import { ExtensionPointId } from "./ExtensionManagerProvider";
19
+ describe("extractExtUrlParams", () => {
20
+ it("should return an empty object when no query string is provided", () => {
21
+ expect(extractExtUrlParams(undefined)).toEqual({});
22
+ });
23
+
24
+ it("should return an empty object when the query string does not contain any valid extension params", () => {
25
+ expect(extractExtUrlParams("foo=bar&baz=qux")).toEqual({});
26
+ });
27
+
28
+ it("should extract valid extension params", () => {
29
+ const queryString =
30
+ "ext=foo&ext.service1.name1.version1=http://example.com";
31
+ const expectedParams = {
32
+ ext: "foo",
33
+ "ext.service1.name1.version1": "http://example.com",
34
+ };
35
+ expect(extractExtUrlParams(queryString)).toEqual(expectedParams);
36
+ });
37
+
38
+ it('should only include params with the "ext" prefix', () => {
39
+ const queryString =
40
+ "ext=foo&other=bar&ext.service1.name1.version1=http://example.com";
41
+ const expectedParams = {
42
+ ext: "foo",
43
+ "ext.service1.name1.version1": "http://example.com",
44
+ };
45
+ expect(extractExtUrlParams(queryString)).toEqual(expectedParams);
46
+ });
47
+ });
48
+
49
+ describe("generateExtensionId", () => {
50
+ it("should replace non-word characters with underscores", () => {
51
+ const url = "http://example.com/some/path";
52
+ expect(generateExtensionId(url)).toBe("http___example_com_some_path");
53
+ });
54
+
55
+ it("should return the same ID when there are no non-word characters", () => {
56
+ const url = "extension_1";
57
+ expect(generateExtensionId(url)).toBe("extension_1");
58
+ });
59
+ });
60
+
61
+ describe("createUrlExtensionsProvider", () => {
62
+ const mockExtensionPointId: ExtensionPointId = {
63
+ service: "service1",
64
+ name: "name1",
65
+ version: "version1",
66
+ };
67
+
68
+ it("should return an ExtensionsProvider that provides installed extensions", async () => {
69
+ const queryString =
70
+ "ext=foo&ext.service1/name1/version1=http://example2.com";
71
+ const provider = createUrlExtensionsProvider(
72
+ mockExtensionPointId,
73
+ queryString
74
+ );
75
+
76
+ const extensions = await provider();
77
+ expect(Object.keys(extensions)).toHaveLength(2);
78
+ expect(extensions).toHaveProperty("foo");
79
+ expect(extensions).toHaveProperty("http___example2_com");
80
+ expect(extensions["http___example2_com"]).toHaveProperty(
81
+ "url",
82
+ "http://example2.com"
83
+ );
84
+ });
85
+
86
+ it("should return an empty object if no valid extensions are found in the query string", async () => {
87
+ const queryString = "foo=bar&baz=qux";
88
+ const provider = createUrlExtensionsProvider(
89
+ mockExtensionPointId,
90
+ queryString
91
+ );
92
+
93
+ const extensions = await provider();
94
+ expect(extensions).toEqual({});
95
+ });
96
+
97
+ it("should filter extensions by the correct extension point", async () => {
98
+ const queryString =
99
+ "ext.service1/name1/version1=http://example1.com&ext.service2/name2/version2=https://www.test.";
100
+ const provider = createUrlExtensionsProvider(
101
+ mockExtensionPointId,
102
+ queryString
103
+ );
104
+
105
+ const extensions = await provider();
106
+ expect(extensions).toHaveProperty("http___example1_com");
107
+ });
108
+
109
+ it("should return an empty object when the query string does not match the expected extension point", async () => {
110
+ const queryString = "ext.service2.name2.version2=http://example1.com";
111
+ const provider = createUrlExtensionsProvider(
112
+ mockExtensionPointId,
113
+ queryString
114
+ );
115
+
116
+ const extensions = await provider();
117
+ expect(extensions).toEqual({});
118
+ });
119
+ });
@@ -0,0 +1,93 @@
1
+ /*************************************************************************
2
+ * ADOBE CONFIDENTIAL
3
+ * ___________________
4
+ *
5
+ * Copyright 2024 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: All information contained herein is, and remains
9
+ * the property of Adobe and its suppliers, if any. The intellectual
10
+ * and technical concepts contained herein are proprietary to Adobe
11
+ * and its suppliers and are protected by all applicable intellectual
12
+ * property laws, including trade secret and copyright laws.
13
+ * Dissemination of this information or reproduction of this material
14
+ * is strictly forbidden unless prior written permission is obtained
15
+ * from Adobe.
16
+ **************************************************************************/
17
+ import { ExtensionsProvider, InstalledExtensions } from "@adobe/uix-host";
18
+ import { Extension } from "@adobe/uix-core";
19
+ import { ExtensionPointId } from "./ExtensionManagerProvider";
20
+
21
+ const EXT_PARAM_PREFIX = "ext";
22
+
23
+ export interface ExtUrlParams {
24
+ [key: string]: string;
25
+ }
26
+
27
+ /**
28
+ * Extracts extension URLs from the query string
29
+ * @ignore
30
+ */
31
+ export function extractExtUrlParams(
32
+ queryString: string | undefined
33
+ ): ExtUrlParams {
34
+ if (!queryString) {
35
+ return {};
36
+ }
37
+ const params: URLSearchParams = new URLSearchParams(queryString);
38
+ return Array.from(params.entries()).reduce((extParams, [key, value]) => {
39
+ if (key === EXT_PARAM_PREFIX || key.startsWith(`${EXT_PARAM_PREFIX}.`)) {
40
+ extParams[key] = value;
41
+ }
42
+ return extParams;
43
+ }, {} as ExtUrlParams);
44
+ }
45
+
46
+ /**
47
+ * Generates an extension ID from the extension URL
48
+ * @ignore
49
+ */
50
+ export function generateExtensionId(extensionUrl: string): string {
51
+ return extensionUrl.replace(/\W/g, "_");
52
+ }
53
+
54
+ /**
55
+ * Creates an ExtensionsProvider that provides extensions from the URL
56
+ * @ignore
57
+ */
58
+ export function createUrlExtensionsProvider(
59
+ extensionPointId: ExtensionPointId,
60
+ queryString: string | undefined
61
+ ): ExtensionsProvider {
62
+ const extUrlParams: ExtUrlParams = extractExtUrlParams(queryString);
63
+
64
+ const extensionUrls: string[] = Object.keys(extUrlParams)
65
+ .filter(
66
+ (extParam) =>
67
+ extParam === EXT_PARAM_PREFIX ||
68
+ extParam ===
69
+ `${EXT_PARAM_PREFIX}.${extensionPointId.service}/${extensionPointId.name}/${extensionPointId.version}`
70
+ )
71
+ .flatMap((extParam) => {
72
+ const paramValue = extUrlParams[extParam];
73
+ // If it's a single value, return it in an array. If it's already an array, return it as is.
74
+ return Array.isArray(paramValue) ? paramValue : [paramValue];
75
+ });
76
+
77
+ const installedExtensions: InstalledExtensions = extensionUrls
78
+ .map((extensionUrl: string) => {
79
+ return {
80
+ id: generateExtensionId(extensionUrl),
81
+ url: extensionUrl,
82
+ extensionPoints: [
83
+ `${extensionPointId.service}/${extensionPointId.name}/${extensionPointId.version}`,
84
+ ],
85
+ } as Extension;
86
+ })
87
+ .reduce((acc: InstalledExtensions, extension: Extension) => {
88
+ acc[extension.id] = extension;
89
+ return acc;
90
+ }, {} as InstalledExtensions);
91
+
92
+ return async () => installedExtensions;
93
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ export * from "./ExtensibleWrapper";
14
+ export * from "./UrlExtensionProvider";
15
+ export * from "./ExtensionManagerProvider";
@@ -13,3 +13,4 @@ governing permissions and limitations under the License.
13
13
  export * from "./Extensible.js";
14
14
  export * from "./GuestUIFrame.js";
15
15
  export * from "./ExtensibleComponentBoundary.js";
16
+ export * from "./ExtensibleWrapper/index.js";
@@ -157,9 +157,6 @@ export function useExtensions<
157
157
  allExtensionPoints
158
158
  )
159
159
  ) {
160
- if (provides) {
161
- guest.provide(provides);
162
- }
163
160
  newExtensions.push(guest as unknown as TypedGuestConnection<Incoming>);
164
161
  }
165
162
  }
@@ -176,6 +173,15 @@ export function useExtensions<
176
173
  );
177
174
 
178
175
  const [extensions, setExtensions] = useState(() => getExtensions());
176
+
177
+ useEffect(() => {
178
+ for (const guest of extensions) {
179
+ if (provides) {
180
+ guest.provide(provides);
181
+ }
182
+ }
183
+ }, [provides, extensions]);
184
+
179
185
  useEffect(() => {
180
186
  return subscribe(() => {
181
187
  setExtensions(getExtensions());