@adobe/uix-guest 0.6.3

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.
@@ -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
+