@financial-times/custom-code-component 1.11.2 → 2.0.1-alpha.11
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.
- package/dist/CustomCodeComponent.d.ts +3 -0
- package/dist/CustomCodeComponent.js +592 -0
- package/dist/CustomCodeComponent.js.map +1 -0
- package/package.json +26 -18
- package/src/custom-code-component.ts +247 -167
- package/src/environment.ts +77 -0
- package/src/errors.ts +73 -0
- package/src/events.ts +22 -0
- package/src/{get-trace.js → get-trace.ts} +23 -14
- package/src/index.ts +8 -0
- package/src/logger.ts +71 -0
- package/src/path.ts +83 -0
- package/src/tracking.ts +20 -11
- package/src/util.ts +66 -0
- package/dist/custom-code-component.d.ts +0 -21
- package/dist/custom-code-component.js +0 -380
- package/dist/tracking.d.ts +0 -24
- 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/
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
27
|
-
tracking
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
40
|
+
log: Logger;
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
constructor() {
|
|
43
|
+
super();
|
|
44
|
+
this.log = new Logger({
|
|
45
|
+
level: convertStringLogLevel(this.getAttribute("log")),
|
|
46
|
+
});
|
|
39
47
|
|
|
40
|
-
const
|
|
41
|
-
|
|
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
|
-
|
|
47
|
-
|
|
51
|
+
try {
|
|
52
|
+
const internals = supportsDeclarative && this.attachInternals();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
this.log.error(e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
80
|
+
emitError(error: Error) {
|
|
81
|
+
let errorEvent;
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
89
|
+
if (!errorEvent) {
|
|
90
|
+
return this.log.debug(error);
|
|
91
|
+
}
|
|
91
92
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
this.dispatchEvent(new ErrorEvent("error", e));
|
|
172
|
-
this.dataset.cccError = "mount-error";
|
|
189
|
+
this.log.error(err);
|
|
173
190
|
|
|
174
|
-
|
|
191
|
+
throw err;
|
|
175
192
|
}
|
|
193
|
+
}
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
|
|
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
|
-
|
|
208
|
+
async load() {
|
|
209
|
+
if (!this.component) {
|
|
210
|
+
throw new Error("No path found")
|
|
211
|
+
}
|
|
188
212
|
const path = this.getAttribute("path");
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
199
285
|
}
|
|
200
|
-
}
|
|
201
286
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
+
}
|