@adobe/uix-guest 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
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 type { GuestApis } from "@adobe/uix-core";
14
+ import type { SharedContext } from "./guest";
15
+ import { Guest } from "./guest";
16
+
17
+ /**
18
+ * A Guest to be used in the "main" or primary frame of an extension, the frame
19
+ * the Host loads first.
20
+ *
21
+ * @remarks This is the Guest object returned from {@link register}. It can
22
+ * expose internal methods to the Host via the {@link GuestServer.register}
23
+ * method.
24
+ *
25
+ *
26
+ * @public
27
+ */
28
+ export class GuestServer<Outgoing extends GuestApis> extends Guest<Outgoing> {
29
+ private localMethods: Outgoing;
30
+ protected getLocalMethods() {
31
+ return {
32
+ ...super.getLocalMethods(),
33
+ apis: this.localMethods,
34
+ };
35
+ }
36
+ /**
37
+ * {@inheritDoc BaseGuest.sharedContext}
38
+ */
39
+ sharedContext: SharedContext;
40
+ /**
41
+ * {@inheritdoc BaseGuest.host}
42
+ */
43
+ host: Guest<Outgoing>["host"];
44
+ /**
45
+ * Pass an interface of methods which Host may call as callbacks.
46
+ *
47
+ * @remarks It is preferable to use {@link register} to obtain a guest object
48
+ * and register local methods in one step. The returned guest object will be
49
+ * pre-registered and connected.
50
+ * @public
51
+ */
52
+ async register(implementedMethods: Outgoing) {
53
+ this.localMethods = implementedMethods;
54
+ return this._connect();
55
+ }
56
+ }
@@ -0,0 +1,123 @@
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 type { RemoteHostApis, VirtualApi } from "@adobe/uix-core";
14
+ import {
15
+ Guest,
16
+ GuestConfig,
17
+ GuestEventBeforeConnect,
18
+ GuestEventConnected,
19
+ GuestEventContextChange,
20
+ GuestEventError,
21
+ } from "./guest";
22
+
23
+ /**
24
+ * A Guest to be used in an extension-controlled frame, usually to display UI.
25
+ *
26
+ * @typeParam Incoming - Optional interface of host methods. If using
27
+ * TypeScript, supply this type parameter and a promisified version of the
28
+ * interface will be available at {@link Guest.host}
29
+ *
30
+ * @remarks
31
+ * This is the object returned when calling {@link @adobe/uix-guest#attach}. It
32
+ * represents an additional frame or runtime created by the host application, on
33
+ * behalf of the extension's control frame which is running the {@link
34
+ * GuestServer}. It is a "secondary" guest object, which a host won't use before
35
+ * the control frame has connected. It exposes a subset of the functionality of
36
+ * the {@link GuestServer}.
37
+ *
38
+ * Unlike the {@link GuestServer}, it cannot register methods or update the
39
+ * {@link Guest.sharedContext}, but it remains in sync with the GuestServer and
40
+ * can access the {@link Guest.sharedContext} of the control frame, as well as
41
+ * any of the published methods on the host.
42
+ *
43
+ * Extensible host apps using the React bindings will likely render GuestUI
44
+ * frames using the {@link @adobe/uix-host-react#GuestUIFrame} component.
45
+ *
46
+ * @example
47
+ * When an extensible app renders this page, {@link @adobe/uix-guest#attach}
48
+ * creates a GuestUI. Once it attaches to the host, it
49
+ * ```javascript
50
+ * import React, { useEffect, useState } from "react";
51
+ * import { attach } from "@adobe/uix-guest";
52
+ * import { Tooltip } from "./tooltip";
53
+ *
54
+ * export default function PopupOverlay(props) {
55
+ * // how large am I?
56
+ * const [dimensions, setDimensions] = useState(
57
+ * document.body.getBoundingClientRect()
58
+ * );
59
+ * // if possible, use language preloaded in query parameters
60
+ * const [language, setLanguage] = useState(props.params.lang)
61
+ *
62
+ * // attach only once, in a useEffect
63
+ * useEffect(() => {
64
+ * attach({
65
+ * id: "my-extension-id",
66
+ * debug: true,
67
+ * })
68
+ * .then(guestUI => {
69
+ * // this event fires whenever the host, or the control frame, changes
70
+ * // any sharedContext value
71
+ * guestUI.addEventListener("contextchange", ({ detail: { context }}) => {
72
+ * setLanguage(context.lang)
73
+ * });
74
+ * // how large does the host want me to be?
75
+ * return guestUI.host.tooltips.getDimensions()
76
+ * .then(setDimensions)
77
+ * })
78
+ * .catch((e) => {
79
+ * console.error("ui attach failed", e);
80
+ * });
81
+ * }, []);
82
+ * // render UI! Due to the setup and useState, this component will re-render
83
+ * // once attach() is complete.
84
+ * return (
85
+ * <Tooltip {...props.params} lang={language} dimensions={dimensions} />
86
+ * );
87
+ * }
88
+ * ```
89
+ *
90
+ * @public
91
+ */
92
+ export class GuestUI<IHost extends VirtualApi> extends Guest<IHost> {
93
+ /**
94
+ * {@inheritDoc Guest."constructor"}
95
+ */
96
+ constructor(config: GuestConfig) {
97
+ super(config);
98
+ }
99
+ /**
100
+ * {@inheritDoc Guest.contextchange}
101
+ * @eventProperty
102
+ */
103
+ public contextchange: GuestEventContextChange;
104
+ /**
105
+ * {@inheritDoc Guest.beforeconnect}
106
+ * @eventProperty
107
+ */
108
+ public beforeconnect: GuestEventBeforeConnect;
109
+ /**
110
+ * {@inheritDoc Guest.connected}
111
+ * @eventProperty
112
+ */
113
+ public connected: GuestEventConnected;
114
+ /**
115
+ * {@inheritDoc Guest.error}
116
+ * @eventProperty
117
+ */
118
+ public error: GuestEventError;
119
+ /**
120
+ * {@inheritDoc Guest.host}
121
+ */
122
+ host: RemoteHostApis<IHost>;
123
+ }
package/src/guest.ts ADDED
@@ -0,0 +1,255 @@
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
+ /* eslint @typescript-eslint/no-explicit-any: "off" */
14
+ import { Connection, connectToParent } from "penpal";
15
+ import type {
16
+ RemoteHostApis,
17
+ HostConnection,
18
+ NamedEvent,
19
+ VirtualApi,
20
+ } from "@adobe/uix-core";
21
+ import {
22
+ Emitter,
23
+ makeNamespaceProxy,
24
+ timeoutPromise,
25
+ quietConsole,
26
+ } from "@adobe/uix-core";
27
+ import { debugGuest } from "./debug-guest.js";
28
+
29
+ /**
30
+ * @public
31
+ */
32
+ export type GuestEvent<
33
+ Type extends string = string,
34
+ Detail = Record<string, unknown>
35
+ > = NamedEvent<
36
+ Type,
37
+ Detail &
38
+ Record<string, unknown> & {
39
+ guest: Guest;
40
+ }
41
+ >;
42
+
43
+ /**
44
+ * @public
45
+ */
46
+ export type GuestEventContextChange = GuestEvent<
47
+ "contextchange",
48
+ { context: Record<string, unknown> }
49
+ >;
50
+
51
+ /** @public */
52
+ export type GuestEventBeforeConnect = GuestEvent<"beforeconnect">;
53
+ /** @public */
54
+ export type GuestEventConnected = GuestEvent<
55
+ "connected",
56
+ { connection: Connection }
57
+ >;
58
+ /** @public */
59
+ export type GuestEventError = GuestEvent<"error", { error: Error }>;
60
+
61
+ /**
62
+ * @public
63
+ */
64
+ export type GuestEvents =
65
+ | GuestEventContextChange
66
+ | GuestEventBeforeConnect
67
+ | GuestEventConnected
68
+ | GuestEventError;
69
+
70
+ /**
71
+ * @public
72
+ */
73
+ export interface GuestConfig {
74
+ /**
75
+ * String slug identifying extension. This may need to use IDs from an
76
+ * external system in the future.
77
+ */
78
+ id: string;
79
+ /**
80
+ * Set debug flags on all libraries that have them, and add loggers to SDK
81
+ * objects. Log a lot to the console.
82
+ */
83
+ debug?: boolean;
84
+ /**
85
+ * Time out and stop trying to reach the host after this many milliseconds
86
+ */
87
+ timeout?: number;
88
+ }
89
+
90
+ /**
91
+ * A `Map` representing the {@link @adobe/uix-host#HostConfig.sharedContext}
92
+ * object.
93
+ *
94
+ * @remarks While the Host object is a plain JavaScript object. the `sharedContext` in the Guest object implements the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map | Map} interface.
95
+ *
96
+ * @example
97
+ * In the host app window, the Host object shares context:
98
+ * ```javascript
99
+ *host.shareContext({
100
+ * someAuthToken: 'abc'
101
+ *});
102
+ * ```
103
+ *
104
+ * After the `contentchange` event has fired in the guest window:
105
+ * ```javascript
106
+ * guest.sharedContext.get('someAuthToken') === 'abc'
107
+ * ```
108
+ * @public
109
+ */
110
+ export class SharedContext {
111
+ private _map: Map<string, unknown>;
112
+ constructor(values: Record<string, unknown>) {
113
+ this.reset(values);
114
+ }
115
+ private reset(values: Record<string, unknown>) {
116
+ this._map = new Map(Object.entries(values));
117
+ }
118
+ /**
119
+ * @public
120
+ * Retrieve a copy of a value from the {@link @adobe/uix-host#HostConfig.sharedContext} object. *Note that this is not a reference to any actual objects from the parent. If the parent updates an "inner object" inside the SharedContext, that change will not be reflected in the Guest!*
121
+ */
122
+ get(key: string) {
123
+ return this._map.get(key);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Generic Guest object, with methods shared by all types of Guest.
129
+ * @internal
130
+ */
131
+ export class Guest<
132
+ Incoming extends object = VirtualApi
133
+ > extends Emitter<GuestEvents> {
134
+ /**
135
+ * Shared context has been set or updated.
136
+ * @eventProperty
137
+ */
138
+ public contextchange: GuestEventContextChange;
139
+ /**
140
+ * About to attempt connection to the host.
141
+ * @eventProperty
142
+ */
143
+ public beforeconnect: GuestEventBeforeConnect;
144
+ /**
145
+ * Host connection has been established.
146
+ * @eventProperty
147
+ */
148
+ public connected: GuestEventConnected;
149
+ /**
150
+ * Host connection has failed.
151
+ * @eventProperty
152
+ */
153
+ public error: GuestEventError;
154
+ /**
155
+ * {@inheritdoc SharedContext}
156
+ */
157
+ sharedContext: SharedContext;
158
+ private debugLogger: Console = quietConsole;
159
+
160
+ /**
161
+ * @param config - Initializer for guest object, including ID.
162
+ */
163
+ constructor(config: GuestConfig) {
164
+ super(config.id);
165
+ if (typeof config.timeout === "number") {
166
+ this.timeout = config.timeout;
167
+ }
168
+ if (config.debug) {
169
+ this.debugLogger = debugGuest(this);
170
+ }
171
+ this.addEventListener("contextchange", (event) => {
172
+ this.sharedContext = new SharedContext(event.detail.context);
173
+ });
174
+ }
175
+ /**
176
+ * Proxy object for calling methods on the host.
177
+ *
178
+ * @remarks Any APIs exposed to the extension via {@link @adobe/uix-host#Port.provide}
179
+ * can be called on this object. Because these methods are called with RPC,
180
+ * they are all asynchronous, The return types of all Host methods will be
181
+ * Promises which resolve to the value the Host method returns.
182
+ * @public
183
+ */
184
+ host: RemoteHostApis<Incoming> = makeNamespaceProxy<Incoming>(
185
+ async (address) => {
186
+ await this.hostConnectionPromise;
187
+ try {
188
+ const result = await timeoutPromise(
189
+ 10000,
190
+ this.hostConnection.invokeHostMethod(address)
191
+ );
192
+ return result;
193
+ } catch (e) {
194
+ const error =
195
+ e instanceof Error ? e : new Error(e as unknown as string);
196
+ const methodError = new Error(
197
+ `Host method call host.${address.path.join(".")}() failed: ${
198
+ error.message
199
+ }`
200
+ );
201
+ this.debugLogger.error(methodError);
202
+ throw methodError;
203
+ }
204
+ }
205
+ );
206
+ private timeout = 10000;
207
+ private hostConnectionPromise: Promise<RemoteHostApis<HostConnection>>;
208
+ private hostConnection!: RemoteHostApis<HostConnection>;
209
+ /** @internal */
210
+ protected getLocalMethods() {
211
+ return {
212
+ emit: (...args: Parameters<typeof this.emit>) => {
213
+ this.debugLogger.log(`Event "${args[0]}" emitted from host`);
214
+ this.emit(...args);
215
+ },
216
+ };
217
+ }
218
+ /**
219
+ * Accept a connection from the Host.
220
+ * @returns A Promise that resolves when the Host has established a connection.
221
+ * @deprecated It is preferable to use {@link register} for primary frames,
222
+ * and {@link attach} for UI frames and other secondary frames, than to
223
+ * instantiate a Guest and then call `.connect()` on it. The latter style
224
+ * returns an object that cannot be used until it is connected, and therefore
225
+ * risks errors.
226
+ * @public
227
+ */
228
+ async connect() {
229
+ return this._connect();
230
+ }
231
+
232
+ /**
233
+ * @internal
234
+ */
235
+ async _connect() {
236
+ this.emit("beforeconnect", { guest: this });
237
+ try {
238
+ const connection = connectToParent<HostConnection<Incoming>>({
239
+ timeout: this.timeout,
240
+ methods: this.getLocalMethods(),
241
+ });
242
+
243
+ this.hostConnectionPromise = connection.promise;
244
+ this.hostConnection = await this.hostConnectionPromise;
245
+ this.sharedContext = new SharedContext(
246
+ await this.hostConnection.getSharedContext()
247
+ );
248
+ this.debugLogger.log("retrieved sharedContext", this.sharedContext);
249
+ this.emit("connected", { guest: this, connection });
250
+ } catch (e) {
251
+ this.emit("error", { guest: this, error: e });
252
+ this.debugLogger.error("Connection failed!", e);
253
+ }
254
+ }
255
+ }
package/src/index.ts ADDED
@@ -0,0 +1,142 @@
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
+ /**
14
+ * @packageDocumentation
15
+ * Tools for UI Extensions meant to run inside extensible apps. Connects
16
+ * Extensions running in their own window contexts with the host app, allowing
17
+ * the host and guest to exchange method, events, and signals.
18
+ *
19
+ * @remarks The core object of this library, which extensions use for
20
+ * communication, is the Guest object. There are two variants of the Guest
21
+ * object {@link GuestServer} for the bootstrap frame which your extension keeps
22
+ * running in the background, and {@link GuestUI} for frames meant to be
23
+ * displayed in the host application. An extension must have one GuestServer
24
+ * frame, and the host app may choose to use one or more GuestUI frames.
25
+ *
26
+ * @example Creating and connecting a GuestServer with {@link register}
27
+ * ```typescript
28
+ * import { register } from "@adobe/uix-guest";
29
+ *
30
+ * const server = await register({
31
+ * // Must match extension ID from registry
32
+ * id: "My Custom View Extension",
33
+ * // enable logging in dev build
34
+ * debug: process.env.NODE_ENV !== "production",
35
+ * // Host can access these methods from its Port to this guest
36
+ * methods: {
37
+ * // Methods must be namespaced by one or more levels
38
+ * myCustomView: {
39
+ * async documentIsViewable(docId) {
40
+ * const doc = await callMyRuntimeAction(docId);
41
+ * return someValidation(doc);
42
+ * },
43
+ * renderView(docId, depth) {
44
+ * // Use a host method
45
+ * const tooltip = await server.host.editor.requestTooltip({
46
+ * type: 'frame',
47
+ * url: new URL(`/show/${docId}`, location).href
48
+ * })
49
+ * }
50
+ * },
51
+ * },
52
+ * })
53
+ * ```
54
+ *
55
+ * @example Connecting to an existing GuestServer with a GuestUI
56
+ * ```typescript
57
+ * import { attach } from "@adobe/uix-guest";
58
+ *
59
+ * const ui = await attach({
60
+ * id: "My Custom View Extension",
61
+ * })
62
+ *
63
+ * // when editing is done:
64
+ * const saved = await ui.host.editor.saveChanges();
65
+ * if (!saved) {
66
+ * const editorState = ui.sharedContext.get('editorState');
67
+ * if (editorState.tooltips[ui.id].invalid === true) {
68
+ * putGuestUIInInvalidState();
69
+ * }
70
+ * } else {
71
+ * ui.host.editor.dismissTooltip();
72
+ * }
73
+ * ```
74
+ *
75
+ */
76
+ import type { Guest, GuestConfig } from "./guest.js";
77
+ import { GuestUI } from "./guest-ui.js";
78
+ import { GuestServer } from "./guest-server.js";
79
+ import { GuestApis } from "@adobe/uix-core";
80
+
81
+ /**
82
+ * {@inheritdoc GuestConfig}
83
+ * @public
84
+ */
85
+ type GuestConfigWithMethods<Outgoing extends GuestApis> = GuestConfig & {
86
+ methods: Outgoing;
87
+ };
88
+
89
+ /**
90
+ * Create and immediately return a {@link GuestServer}.
91
+ *
92
+ * @deprecated Use {@link attach} or {@link register}, which return Promises
93
+ * that resolve once the guest is connected.
94
+ * @public
95
+ */
96
+ export function createGuest(config: GuestConfig) {
97
+ const guest = new GuestServer(config);
98
+ return guest;
99
+ }
100
+
101
+ /**
102
+ * Connect to a running {@link GuestServer} to share its context and render UI.
103
+ *
104
+ * @remarks Creates a guest object that shares most of the GuestServer API,
105
+ * except it cannot register its own methods. Use `attach()` in an app or
106
+ * document that is meant to render a UI in the host application; it will have
107
+ * access to the sharedContext object shared by the host and GuestServer.
108
+ *
109
+ * @public
110
+ */
111
+ export async function attach(config: GuestConfig) {
112
+ const guest = new GuestUI(config);
113
+ await guest._connect();
114
+ return guest;
115
+ }
116
+
117
+ /**
118
+ * Initiate a connection to the host app and its extension points.
119
+ *
120
+ * @remarks Creates the "main" {@link GuestServer}, which runs in the background
121
+ * without UI. Registers methods passed in the `methods` parameter, then
122
+ * resolves the returned Promise with the connected GuestServer object.
123
+ *
124
+ * @public
125
+ */
126
+ export async function register<Outgoing extends GuestApis>(
127
+ config: GuestConfigWithMethods<Outgoing>
128
+ ) {
129
+ const guest = new GuestServer(config);
130
+ await guest.register(config.methods);
131
+ return guest;
132
+ }
133
+
134
+ // backwards compatibility
135
+ export {
136
+ Guest,
137
+ Guest as BaseGuest,
138
+ GuestUI,
139
+ GuestUI as UIGuest,
140
+ GuestServer,
141
+ GuestServer as PrimaryGuest,
142
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig-base.json",
4
+ "compilerOptions": {
5
+ "outDir": "dist",
6
+ "rootDir": "src"
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ],
11
+ "exclude": [
12
+ "src/**/*.test.tsx?"
13
+ ],
14
+ "references": [
15
+ {
16
+ "path": "../uix-core"
17
+ }
18
+ ]
19
+ }
20
+