@adobe/uix-host-react 0.10.4 → 1.0.0

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 (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());