@financial-times/custom-code-component 1.11.1 → 2.0.1-alpha.10

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 (35) hide show
  1. package/dist/index.js +598 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/webcomponent/src/custom-code-component.d.ts +55 -0
  4. package/dist/webcomponent/src/environment.d.ts +15 -0
  5. package/dist/webcomponent/src/errors.d.ts +27 -0
  6. package/dist/webcomponent/src/events.d.ts +10 -0
  7. package/dist/webcomponent/src/get-trace.d.ts +8 -0
  8. package/dist/webcomponent/src/index.d.ts +1 -0
  9. package/dist/webcomponent/src/logger.d.ts +20 -0
  10. package/dist/webcomponent/src/path.d.ts +23 -0
  11. package/dist/{tracking.d.ts → webcomponent/src/tracking.d.ts} +12 -4
  12. package/dist/webcomponent/src/util.d.ts +33 -0
  13. package/dist/webcomponent/test/environment.test.d.ts +1 -0
  14. package/dist/webcomponent/test/error-handling.test.d.ts +8 -0
  15. package/dist/webcomponent/test/example.d.ts +11 -0
  16. package/dist/webcomponent/test/generate-readable-stream.d.ts +8 -0
  17. package/dist/webcomponent/test/path.test.d.ts +5 -0
  18. package/dist/webcomponent/test/ssr.test.d.ts +4 -0
  19. package/dist/webcomponent/test/utils.test.d.ts +1 -0
  20. package/dist/webcomponent/vite.config.d.ts +2 -0
  21. package/dist/webcomponent/vitest.config.d.ts +2 -0
  22. package/package.json +25 -17
  23. package/src/custom-code-component.ts +247 -167
  24. package/src/environment.ts +77 -0
  25. package/src/errors.ts +73 -0
  26. package/src/events.ts +22 -0
  27. package/src/{get-trace.js → get-trace.ts} +23 -14
  28. package/src/index.ts +8 -0
  29. package/src/logger.ts +71 -0
  30. package/src/path.ts +83 -0
  31. package/src/tracking.ts +20 -11
  32. package/src/util.ts +66 -0
  33. package/dist/custom-code-component.d.ts +0 -21
  34. package/dist/custom-code-component.js +0 -380
  35. package/src/custom-code-component.xsd +0 -67
@@ -3,15 +3,24 @@
3
3
  * Main component definition for custom-code-component
4
4
  */
5
5
 
6
- import { ContentTree } from "@financial-times/content-tree";
7
- import { BaseRenderer } from "../../ccc-sdk/src/renderers/AbstractBaseRenderer";
6
+ import type { ContentTree } from "@financial-times/content-tree";
7
+ import { BaseRenderer } from "../../ccc-sdk/src/renderers/BaseRenderer";
8
8
  import Tracking from "./tracking";
9
-
10
- class FTCustomCodeComponent extends HTMLElement {
11
- app: typeof BaseRenderer.prototype.render;
12
-
13
- mode: "closed" | "open";
14
-
9
+ import { convertStringLogLevel, Logger } from "./logger";
10
+ import {
11
+ CCCError,
12
+ CCCImportError,
13
+ CCCRenderError,
14
+ CCCTimeoutError,
15
+ } from "./errors";
16
+ import { CCCConnectedEvent } from "./events";
17
+ import { ComponentPath } from "./path";
18
+ import { kebabize } from "./util";
19
+ import { assignTestURL, useComponentTestEnv } from "./environment";
20
+
21
+ export class FTCustomCodeComponent extends HTMLElement {
22
+ app?: typeof BaseRenderer.prototype.render;
23
+ mode: "closed" | "open" = "open";
15
24
  RESERVED_ATTRS = new Set([
16
25
  "iframe",
17
26
  "path",
@@ -23,188 +32,271 @@ class FTCustomCodeComponent extends HTMLElement {
23
32
  "load-timeout",
24
33
  ]);
25
34
 
26
- source: string;
27
- tracking: Tracking;
28
-
29
- async mount() {
30
- if (!this.app) {
31
- throw new Error("CCC mounted without App");
32
- }
33
-
34
- const shadow = this.shadowRoot ?? this.attachShadow({ mode: this.mode });
35
+ source?: string;
36
+ tracking?: Tracking;
37
+ lightRoot?: ChildNode[];
38
+ component?: ComponentPath;
35
39
 
36
- const App = this.app;
40
+ log: Logger;
37
41
 
38
- const data = JSON.parse(this.getAttribute("data-component-props")!);
42
+ constructor() {
43
+ super();
44
+ this.log = new Logger({
45
+ level: convertStringLogLevel(this.getAttribute("log")),
46
+ });
39
47
 
40
- const extraProps = Object.fromEntries(
41
- [...this.attributes]
42
- .filter((attribute) => !this.RESERVED_ATTRS.has(attribute.name))
43
- .map((attribute) => [attribute.name, attribute.value])
44
- );
48
+ const supportsDeclarative =
49
+ HTMLElement.prototype.hasOwnProperty("attachInternals");
45
50
 
46
- // Clear old children
47
- // this.shadowRoot?.replaceChildren();
51
+ try {
52
+ const internals = supportsDeclarative && this.attachInternals();
53
+ } catch (e) {
54
+ this.log.error(e);
55
+ }
56
+ }
48
57
 
49
- // Create tracking instance
50
- this.tracking = new Tracking({
51
- name: `${this.getAttribute("path")}@${this.getAttribute("version")}`,
52
- subtype: "interactive",
53
- teamName: "djd",
54
- shadowRoot: this.shadowRoot,
55
- });
58
+ async connectedCallback() {
59
+ try {
60
+ const path = this.getAttribute("path");
61
+ const versionRange = this.getAttribute("version");
62
+ this.component = ComponentPath.fromString(path, versionRange);
63
+
64
+ // Backup the light root to restore in case of error
65
+ this.lightRoot = Array.from(this.childNodes);
66
+ this.app = await this.load();
67
+ await this.mount();
68
+ await this.initTracking();
69
+ } catch (e) {
70
+ if (e instanceof Error) {
71
+ requestAnimationFrame(() => {
72
+ this.emitError(e);
73
+ });
74
+ }
56
75
 
57
- const { unmount, onmessage } =
58
- App(
59
- shadow,
60
- {
61
- ...extraProps,
62
- data,
63
- port: this.channel.port2,
64
- tracking: this.tracking,
65
- },
66
- ...this.children
67
- ) || {};
68
-
69
- if (unmount) this.unmount = unmount;
70
- if (onmessage) this.onmessage = onmessage;
76
+ this.unmount(e as Error);
77
+ }
71
78
  }
72
79
 
73
- unmount = (root?: any) => {};
80
+ emitError(error: Error) {
81
+ let errorEvent;
74
82
 
75
- async connectedCallback() {
76
- const path = this.getAttribute("path");
77
- const componentVersionRange = this.getAttribute("version");
78
- const timeout = this.getAttribute("load-timeout") ?? 2000;
79
- const useLocalVersion = this.getAttribute("env")
80
- ?.toLowerCase()
81
- .startsWith("d");
82
-
83
- if (!path)
84
- throw new Error(
85
- "path attribute not specified in <custom-code-component>"
86
- );
83
+ if (error instanceof CCCError && error.name?.startsWith("CCC")) {
84
+ errorEvent = error.name.replace(/^CCC/, "ccc:");
85
+ } else {
86
+ errorEvent = `ccc:${error?.name ?? "UnknownError"}`;
87
+ }
87
88
 
88
- const [componentName, componentRepo, componentOrg] = path
89
- .split("/")
90
- .reverse();
89
+ if (!errorEvent) {
90
+ return this.log.debug(error);
91
+ }
91
92
 
92
- if (!componentName || !componentRepo || !componentOrg) return;
93
+ this.dispatchEvent(
94
+ new ErrorEvent(errorEvent, {
95
+ bubbles: true,
96
+ cancelable: false,
97
+ composed: true,
98
+ error,
99
+ message: error.message,
100
+ })
101
+ );
102
+ }
93
103
 
94
- const id = this.getAttribute('id');
104
+ disconnectedCallback() {
105
+ const path = this.getAttribute("path");
106
+ this.log.info(`<custom-code-component:${path}> disconnected`);
107
+ if (typeof this.onunmount === "function") this.onunmount();
108
+ }
95
109
 
96
- // id querystring necessary to multiple allow components with the same source (same name and version number) to appear on the page correctly
97
- this.source = useLocalVersion
98
- ? `http://localhost:5173/src/${componentName}/index.jsx?id=${id}`
99
- : `https://www.ft.com/__component/${componentOrg}/${componentRepo}${
100
- componentVersionRange ? `@${componentVersionRange}` : "@latest"
101
- }/${componentName}/${componentName}.js?id=${id}`;
102
-
110
+ channel = new MessageChannel();
103
111
 
112
+ onmessage() {}
113
+ onunmount(root?: any) {}
114
+ async onready(app: Promise<void>) {
104
115
  try {
105
- this.app = await new Promise((resolve, reject) => {
106
- const to = setTimeout(() => {
107
- this.dispatchEvent(
108
- new CustomEvent("ccc-timeout", {
109
- bubbles: true,
110
- cancelable: true,
111
- detail: {
112
- component: `${path}@${componentVersionRange}`,
113
- source: this.source,
114
- },
115
- })
116
- );
117
- this.dataset.cccError = "import-timeout";
118
- delete this.dataset.cccReady;
119
- }, Number(timeout));
120
- import(/* webpackIgnore: true */ this.source /* @vite-ignore */)
121
- .then(({ default: componentRenderFunction }) => {
122
- if (componentRenderFunction) {
123
- clearTimeout(to);
124
- resolve(componentRenderFunction);
125
- } else
126
- throw new CCCImportError(
127
- "No component renderer default export found"
128
- );
129
- })
130
- .catch((e) => reject(new CCCImportError(e)));
131
- });
116
+ await app;
132
117
  } catch (e) {
133
- console.error(
134
- `<custom-code-component> error during import from ${path}@${componentVersionRange}`
135
- );
136
-
137
- delete this.dataset.cccReady;
138
- this.dataset.cccError = "import-failure";
139
-
140
- this.dispatchEvent(new ErrorEvent("error", e));
141
- console.error(e);
142
-
143
- return; // prevent mounting on caught error
118
+ if (e instanceof Error) this.emitError(e);
144
119
  }
120
+ }
145
121
 
122
+ postMessage(event: any) {
123
+ this.channel.port1.postMessage(event);
124
+ }
125
+
126
+ async mount(prerendered?: ShadowRoot | null) {
146
127
  try {
147
128
  this.mode =
148
129
  this.getAttribute("shadow-open") == "false" ? "closed" : "open";
130
+ if (this.component) {
149
131
 
150
- await this.mount();
151
-
152
- this.dispatchEvent(
153
- new CustomEvent("ccc-connected", {
154
- bubbles: true,
155
- cancelable: true,
156
- detail: {
157
- component: `${path}@${componentVersionRange}`,
132
+ this.dispatchEvent(
133
+ new CCCConnectedEvent({
134
+ component: this.component,
158
135
  source: this.source,
159
- },
160
- })
161
- );
162
-
163
- this.dataset.cccReady = "true";
164
- delete this.dataset.cccError;
165
- } catch (e) {
166
- console.info(
167
- `<custom-code-component> uncaught error during mount from ${path}@${componentVersionRange}`
136
+ })
137
+ );
138
+
139
+ this.dataset.cccReady = "true";
140
+ delete this.dataset.cccError;
141
+
142
+
143
+ if (!this.app) {
144
+ throw new Error("CCC mounted without App");
145
+ }
146
+
147
+ const ssr = this.shadowRoot !== null;
148
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: this.mode });
149
+
150
+ const data = JSON.parse(this.getAttribute("data-component-props")!);
151
+
152
+ const extraProps = Object.fromEntries(
153
+ [...this.attributes]
154
+ .filter((attribute) => !this.RESERVED_ATTRS.has(attribute.name))
155
+ .map((attribute) => [attribute.name, attribute.value])
156
+ );
157
+
158
+ // Create tracking instance
159
+ this.tracking = new Tracking({
160
+ name: this.component?.toString(),
161
+ subtype: "interactive",
162
+ teamName: "djd",
163
+ shadowRoot: this.shadowRoot,
164
+ logger: this.log,
165
+ });
166
+
167
+ const { unmount, onmessage, ready } =
168
+ this.app(
169
+ shadow,
170
+ {
171
+ ...extraProps,
172
+ data,
173
+ port: this.channel.port2,
174
+ tracking: this.tracking,
175
+ prerendered: !!prerendered,
176
+ children: this.children,
177
+ },
178
+ ssr
179
+ ) || {};
180
+
181
+ if (unmount) this.onunmount = unmount;
182
+ if (onmessage) this.onmessage = onmessage;
183
+ if (ready) this.onready(ready);
184
+ }
185
+ } catch (err) {
186
+ this.log.info(
187
+ `<custom-code-component> uncaught error during mount from ${this.component?.toString()}`
168
188
  );
169
- console.error(e);
170
-
171
- this.dispatchEvent(new ErrorEvent("error", e));
172
- this.dataset.cccError = "mount-error";
189
+ this.log.error(err);
173
190
 
174
- delete this.dataset.cccReady;
191
+ throw err;
175
192
  }
193
+ }
176
194
 
177
- try {
178
- this.tracking.init(this.id);
179
- } catch (e) {
180
- console.info(
181
- `Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`
182
- );
183
- console.error(e);
195
+ // Called in top-level error handler
196
+ // Replace shadow root with light DOM children on error
197
+ unmount(e: Error) {
198
+ if (this.lightRoot) {
199
+ this.onunmount();
200
+ this.shadowRoot?.replaceChildren(...this.lightRoot);
201
+
202
+ if (!this.dataset.cccError)
203
+ this.dataset.cccError = kebabize(e.name.replace("CCC", ""));
204
+ delete this.dataset.cccReady;
184
205
  }
185
206
  }
186
207
 
187
- disconnectedCallback() {
208
+ async load() {
209
+ if (!this.component) {
210
+ throw new Error("No path found")
211
+ }
188
212
  const path = this.getAttribute("path");
189
- console.info(`<custom-code-component:${path}> disconnected`);
190
- if (typeof this.unmount === "function") this.unmount();
191
- }
213
+ const componentVersionRange = this.getAttribute("version");
214
+ const timeout = Number(this.getAttribute("load-timeout") || 10000);
215
+ const testEnv = this.getAttribute("testEnv")
216
+ const testUrl = assignTestURL(testEnv)
217
+ const isTestEnv = await useComponentTestEnv(testUrl)
192
218
 
193
- channel = new MessageChannel();
219
+ const id = this.getAttribute("id");
220
+ // id querystring necessary to multiple allow components with the same source (same name and version number) to appear on the page correctly
221
+ this.source = isTestEnv
222
+ ? `${testUrl?.origin}/src/${this.component.component}/index.jsx?id=${id}`
223
+ : `https://www.ft.com/__component/${this.component.org}/${this.component.repo}${
224
+ componentVersionRange ? `@${componentVersionRange}` : "@latest"
225
+ }/${this.component.component}/${this.component.component}.js?id=${id}`;
194
226
 
195
- onmessage() {} // I'm honestly not sure what to do with this
227
+ try {
228
+ return await new Promise<typeof BaseRenderer.prototype.render>(
229
+ (resolve, reject) => {
230
+ const to = setTimeout(() => {
231
+ this.log.error("CCC import timeout error");
232
+ reject(
233
+ new CCCTimeoutError({
234
+ component: this.component!,
235
+ source: this.source,
236
+ })
237
+ );
238
+ }, Number(timeout));
239
+
240
+ if (this.source) {
241
+ import(/* webpackIgnore: true */ this.source /* @vite-ignore */)
242
+ .then(({ default: componentRenderFunction }) => {
243
+ if (componentRenderFunction) {
244
+ clearTimeout(to);
245
+ resolve(componentRenderFunction);
246
+ } else
247
+ throw new CCCImportError(
248
+ "No component renderer default export found",
249
+ {
250
+ component: this.component!,
251
+ source: this.source,
252
+ }
253
+ );
254
+ })
255
+ .catch((e) => {
256
+ clearTimeout(to);
257
+ this.log.error(e);
258
+ if (e instanceof Error && !(e instanceof CCCImportError)) {
259
+ reject(
260
+ new CCCImportError(e.message, {
261
+ component: this.component!,
262
+ source: this.source,
263
+ })
264
+ );
265
+ } else {
266
+ reject(e);
267
+ }
268
+ });
269
+ } else {
270
+ clearTimeout(to);
271
+ throw new CCCImportError(`Unable to mount ${path}`, {
272
+ component: this.component!,
273
+ source: this.source,
274
+ });
275
+ }
276
+ }
277
+ );
278
+ } catch (err) {
279
+ this.log.error(
280
+ `<custom-code-component> error during import from ${path}@${componentVersionRange}`
281
+ );
196
282
 
197
- postMessage(event: any) {
198
- this.channel.port1.postMessage(event);
283
+ throw err;
284
+ }
199
285
  }
200
- }
201
286
 
202
- // Register the custom element
203
- export const init = () =>
204
- customElements.define("custom-code-component", FTCustomCodeComponent);
205
-
206
- // Allows module to be imported multiple times but custom element only registered once
207
- if (customElements && !customElements.get("custom-code-component")) init();
287
+ initTracking = async () => {
288
+ try {
289
+ this.tracking?.init(this.id);
290
+ } catch (e) {
291
+ const path = this.getAttribute("path");
292
+ const componentVersionRange = this.getAttribute("version");
293
+ this.log.info(
294
+ `Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`
295
+ );
296
+ this.log.error(e);
297
+ }
298
+ };
299
+ }
208
300
 
209
301
  export interface CustomCodeComponent extends ContentTree.Node {
210
302
  type: "CustomCodeComponent";
@@ -221,16 +313,4 @@ export interface CustomCodeComponent extends ContentTree.Node {
221
313
  } | { children?: CustomCodeComponent | Array<CustomCodeComponent> };
222
314
  }
223
315
 
224
- class CCCImportError extends Error {
225
- constructor(...params) {
226
- // Pass remaining arguments (including vendor specific ones) to parent constructor
227
- super(...params);
228
-
229
- // Maintains proper stack trace for where our error was thrown (only available on V8)
230
- if (Error.captureStackTrace) {
231
- Error.captureStackTrace(this, CCCImportError);
232
- }
233
-
234
- this.name = "CCCImportError";
235
- }
236
- }
316
+ export type { FTCustomCodeComponent as CCCHTMLElement };
@@ -0,0 +1,77 @@
1
+ import { isLocalEnv, isSafeTestEnv, isSparkEnv } from "./util"
2
+
3
+
4
+ /**
5
+ * Assigns a test URL based on the provided testEnv value.
6
+ *
7
+ * @param testEnv - A string indicating test environment setting
8
+ * @returns A `URL` object if valid and safe, or `undefined` if invalid.
9
+ */
10
+ export function assignTestURL(testEnv: string | null): URL | undefined {
11
+ if (testEnv === null) {
12
+ return
13
+ }
14
+
15
+ let testUrl
16
+ const defaultTestUrl = new URL('http://localhost:5173')
17
+
18
+ try {
19
+ if (typeof testEnv === 'string') {
20
+ if ((testEnv === '' || testEnv.toLowerCase() === 'true') && isLocalEnv()) {
21
+ testUrl = defaultTestUrl
22
+ } else {
23
+ const hasProtocol = testEnv.startsWith('http://') || testEnv.startsWith('https://')
24
+ testUrl = hasProtocol ? new URL(testEnv) : undefined
25
+
26
+ // Prevent script injection
27
+ if (testUrl && !isSafeTestEnv(testUrl?.hostname)) {
28
+ throw new Error("Unsafe testing host override");
29
+ }
30
+ }
31
+ } else if (isSparkEnv()) {
32
+ testUrl = defaultTestUrl;
33
+ }
34
+ } catch (_) {
35
+ return testUrl
36
+ }
37
+
38
+ return testUrl
39
+ }
40
+
41
+ /**
42
+ * Checks whether the given test URL can establish a successful WebSocket connection,
43
+ * which indicates a live test environment (e.g., Vite HMR is active).
44
+ *
45
+ * @param testUrl - The test environment URL to check.
46
+ * @returns A promise that resolves to true if WebSocket connection is open, otherwise false.
47
+ */
48
+ export async function useComponentTestEnv(testUrl?: URL): Promise<boolean> {
49
+ if (!testUrl) {
50
+ return false
51
+ }
52
+
53
+ function checkWebSocketOpen(host: string): Promise<boolean> {
54
+ try {
55
+ return new Promise<boolean>((res) => {
56
+ const socket = new WebSocket(`ws://${host}`, "vite-hmr");
57
+
58
+ const timer = setTimeout(() => {
59
+ res(socket.readyState === WebSocket.OPEN);
60
+ socket.close();
61
+ }, 50);
62
+
63
+ socket.addEventListener("error", () => {
64
+ clearTimeout(timer);
65
+ socket.close();
66
+ res(false);
67
+ }, { once: true });
68
+ });
69
+ } catch (err) {
70
+ return Promise.resolve(false);
71
+ }
72
+ }
73
+
74
+ const isWebSocketOpen = await checkWebSocketOpen(testUrl?.host);
75
+
76
+ return isWebSocketOpen
77
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * This is the base CCC component error class. These are raised when the component being loaded errors
3
+ */
4
+
5
+ import { ComponentPath, DetailType, isValidComponentPathObject } from "./path";
6
+
7
+ export class CCCError extends Error {
8
+ component: ComponentPath | null;
9
+ source?: string;
10
+ constructor(message: string | null, detail?: DetailType) {
11
+ if (!detail && message) {
12
+ super(message);
13
+ this.component = null;
14
+ } else if (typeof detail?.component === "string") {
15
+ super(
16
+ message ??
17
+ `${detail.cause ?? "Unknown error"} in ${detail.component} imported from ${detail.source ?? "an undefined source"}.`
18
+ );
19
+
20
+ this.component = ComponentPath.fromString(detail.component);
21
+ } else if (isValidComponentPathObject(detail?.component)) {
22
+ super(
23
+ message ??
24
+ `${detail.cause ?? "Unknown error"} in ${detail.component.org}/${detail.component.repo}/${detail.component.component}@${detail.component.versionRange} imported from ${detail.source ?? "an undefined source"}.`
25
+ );
26
+ this.component = new ComponentPath(detail.component);
27
+ } else {
28
+ super(
29
+ `${detail?.cause ?? "Unknown error"} in unknown component imported from ${detail?.source ?? "unknown source"}.`
30
+ );
31
+ this.component = null;
32
+ }
33
+
34
+ this.source = detail?.source;
35
+
36
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
37
+ if (Error.captureStackTrace) {
38
+ Error.captureStackTrace(this, CCCError);
39
+ }
40
+
41
+ this.name = "CCCError";
42
+ }
43
+ }
44
+
45
+ /**
46
+ * This class is used to raise errors that occur during the import of the CCC component app
47
+ */
48
+ export class CCCImportError extends CCCError {
49
+ constructor(message: string, detail: DetailType) {
50
+ super(message, { ...detail, cause: "Import error" });
51
+ this.name = "CCCImportError";
52
+ }
53
+ }
54
+
55
+ /**
56
+ * This class is used to raise errors that occur during the rendering of the CCC component app
57
+ */
58
+ export class CCCRenderError extends CCCError {
59
+ constructor(message: string, detail: DetailType) {
60
+ super(message, { ...detail, cause: "Render error" });
61
+ this.name = "CCCRenderError";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * This class is used to raise errors that occur as a result of the CCC component app timing out
67
+ */
68
+ export class CCCTimeoutError extends CCCError {
69
+ constructor(detail: DetailType) {
70
+ super(null, { ...detail, cause: "Timeout error" });
71
+ this.name = "CCCTimeoutError";
72
+ }
73
+ }
package/src/events.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { ComponentPath } from "./path";
2
+
3
+ export class CCCConnectedEvent extends Event {
4
+ static eventType = "ccc:ConnectedEvent";
5
+
6
+ component: ComponentPath;
7
+ source?: string;
8
+
9
+ constructor(
10
+ detail: { component: ComponentPath; source?: string },
11
+ opts?: EventInit
12
+ ) {
13
+ super(CCCConnectedEvent.eventType, {
14
+ bubbles: true,
15
+ cancelable: false,
16
+ composed: true,
17
+ ...opts,
18
+ });
19
+ this.component = detail.component;
20
+ this.source = detail.source;
21
+ }
22
+ }