@financial-times/custom-code-component 2.0.1-beta.8 → 2.0.2
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/custom-element.d.ts +8 -4
- package/dist/custom-element.js +223 -141
- package/dist/custom-element.js.map +1 -1
- package/package.json +2 -1
- package/src/custom-code-component.css +9 -9
- package/src/custom-code-component.ts +116 -38
- package/src/events.ts +46 -4
- package/src/path.ts +9 -3
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
CCCRenderError,
|
|
14
14
|
CCCTimeoutError,
|
|
15
15
|
} from "./errors";
|
|
16
|
-
import { CCCConnectedEvent } from "./events";
|
|
16
|
+
import { CCCConnectedEvent, CCCReadyEvent, CCCViewportEvent } from "./events";
|
|
17
17
|
import { ComponentPath } from "./path";
|
|
18
18
|
import { kebabize } from "./util";
|
|
19
19
|
import { assignTestURL, useComponentTestEnv } from "./environment";
|
|
@@ -35,10 +35,24 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
35
35
|
|
|
36
36
|
source?: string;
|
|
37
37
|
tracking?: Tracking;
|
|
38
|
-
|
|
38
|
+
testUrl?: URL;
|
|
39
|
+
component = new ComponentPath();
|
|
39
40
|
|
|
40
41
|
log: Logger;
|
|
41
42
|
|
|
43
|
+
observer = new IntersectionObserver((entries) => {
|
|
44
|
+
entries.forEach((entry) => {
|
|
45
|
+
this.dispatchEvent(
|
|
46
|
+
new CCCViewportEvent({
|
|
47
|
+
component: this.component,
|
|
48
|
+
source: this.source,
|
|
49
|
+
intersecting: entry.isIntersecting,
|
|
50
|
+
entry,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
42
56
|
constructor() {
|
|
43
57
|
super();
|
|
44
58
|
this.log = new Logger({
|
|
@@ -76,19 +90,40 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
emitError(error: Error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
if (error instanceof CCCError) {
|
|
94
|
+
this.dispatchEvent(
|
|
95
|
+
new ErrorEvent("ccc:error", {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
cancelable: false,
|
|
98
|
+
composed: true,
|
|
99
|
+
error,
|
|
100
|
+
message: error.message,
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
// Wrap original error with generic CCC error class
|
|
105
|
+
const wrappedError = new CCCError(error.message, {
|
|
106
|
+
component: this.component,
|
|
107
|
+
error: error,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.dispatchEvent(
|
|
111
|
+
new ErrorEvent("ccc:error", {
|
|
112
|
+
bubbles: true,
|
|
113
|
+
cancelable: false,
|
|
114
|
+
composed: true,
|
|
115
|
+
error: wrappedError,
|
|
116
|
+
message: wrappedError.message,
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
}
|
|
86
120
|
}
|
|
87
121
|
|
|
88
122
|
disconnectedCallback() {
|
|
89
123
|
const path = this.getAttribute("path");
|
|
90
124
|
this.log.info(`<custom-code-component:${path}> disconnected`);
|
|
91
125
|
if (typeof this.onunmount === "function") this.onunmount();
|
|
126
|
+
this.observer.disconnect();
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
channel = new MessageChannel();
|
|
@@ -100,11 +135,23 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
100
135
|
async onready(app: Promise<void>) {
|
|
101
136
|
try {
|
|
102
137
|
await app;
|
|
138
|
+
|
|
139
|
+
this.dispatchEvent(
|
|
140
|
+
new CCCReadyEvent({
|
|
141
|
+
component: this.component,
|
|
142
|
+
source: this.source,
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
this.dataset.cccReady = "true";
|
|
147
|
+
delete this.dataset.cccError;
|
|
148
|
+
|
|
149
|
+
this.observer.observe(this);
|
|
103
150
|
} catch (e) {
|
|
104
151
|
if (e instanceof Error) {
|
|
105
152
|
const renderError = new CCCRenderError(e.message, {
|
|
106
153
|
error: e,
|
|
107
|
-
component: this.component
|
|
154
|
+
component: this.component,
|
|
108
155
|
});
|
|
109
156
|
requestAnimationFrame(() => {
|
|
110
157
|
this.emitError(renderError);
|
|
@@ -123,16 +170,6 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
123
170
|
this.mode =
|
|
124
171
|
this.getAttribute("shadow-open") == "false" ? "closed" : "open";
|
|
125
172
|
if (this.component) {
|
|
126
|
-
this.dispatchEvent(
|
|
127
|
-
new CCCConnectedEvent({
|
|
128
|
-
component: this.component,
|
|
129
|
-
source: this.source,
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
this.dataset.cccReady = "true";
|
|
134
|
-
delete this.dataset.cccError;
|
|
135
|
-
|
|
136
173
|
if (!this.app) {
|
|
137
174
|
throw new Error("CCC mounted without App");
|
|
138
175
|
}
|
|
@@ -141,6 +178,12 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
141
178
|
const shadow =
|
|
142
179
|
this.shadowRoot ?? this.attachShadow({ mode: this.mode });
|
|
143
180
|
|
|
181
|
+
this.dispatchEvent(
|
|
182
|
+
new CCCConnectedEvent({
|
|
183
|
+
component: this.component,
|
|
184
|
+
source: this.source,
|
|
185
|
+
})
|
|
186
|
+
);
|
|
144
187
|
// Add global CCC styles if not already present in shadow DOM.
|
|
145
188
|
if (
|
|
146
189
|
!shadow.querySelector('link[href~="custom-code-component.css"]') &&
|
|
@@ -194,19 +237,22 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
194
237
|
);
|
|
195
238
|
}
|
|
196
239
|
|
|
197
|
-
const {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
const {
|
|
241
|
+
unmount,
|
|
242
|
+
onmessage,
|
|
243
|
+
ready = Promise.resolve(),
|
|
244
|
+
} = this.app(
|
|
245
|
+
shadow,
|
|
246
|
+
{
|
|
247
|
+
...(data ?? extraProps),
|
|
248
|
+
data: data ?? extraProps,
|
|
249
|
+
port: this.channel.port2,
|
|
250
|
+
tracking: this.tracking,
|
|
251
|
+
prerendered: !!prerendered,
|
|
252
|
+
children: this.children,
|
|
253
|
+
},
|
|
254
|
+
ssr
|
|
255
|
+
) || {};
|
|
210
256
|
|
|
211
257
|
if (unmount) this.onunmount = unmount;
|
|
212
258
|
if (onmessage) this.onmessage = onmessage;
|
|
@@ -240,23 +286,33 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
240
286
|
this.dataset.cccError = kebabize(e.name.replace("CCC", ""));
|
|
241
287
|
|
|
242
288
|
delete this.dataset.cccReady;
|
|
289
|
+
|
|
290
|
+
this.observer.disconnect();
|
|
243
291
|
}
|
|
244
292
|
|
|
245
293
|
async load() {
|
|
246
|
-
if (!this.component) {
|
|
294
|
+
if (!this.component.isValid) {
|
|
247
295
|
throw new Error("No path found");
|
|
248
296
|
}
|
|
249
297
|
const path = this.getAttribute("path");
|
|
250
298
|
const componentVersionRange = this.getAttribute("version");
|
|
251
299
|
const timeout = Number(this.getAttribute("load-timeout") || 10000);
|
|
252
300
|
const testEnv = this.getAttribute("test-env");
|
|
253
|
-
|
|
254
|
-
const isTestEnv = await useComponentTestEnv(
|
|
301
|
+
this.testUrl = assignTestURL(testEnv);
|
|
302
|
+
const isTestEnv = await useComponentTestEnv(
|
|
303
|
+
this.component.name,
|
|
304
|
+
this.testUrl
|
|
305
|
+
);
|
|
255
306
|
|
|
256
|
-
const id = this.getAttribute("id");
|
|
257
307
|
// id querystring necessary to multiple allow components with the same source (same name and version number) to appear on the page correctly
|
|
308
|
+
const id = this.getAttribute("id");
|
|
309
|
+
|
|
310
|
+
if (isTestEnv && this.testUrl) {
|
|
311
|
+
this.injectViteScripts();
|
|
312
|
+
}
|
|
313
|
+
|
|
258
314
|
this.source = isTestEnv
|
|
259
|
-
? `${testUrl?.origin}/src/${this.component.name}/index.jsx?id=${id}`
|
|
315
|
+
? `${this.testUrl?.origin}/src/${this.component.name}/index.jsx?id=${id}`
|
|
260
316
|
: `https://www.ft.com/__component/${this.component.org}/${this.component.repo}${
|
|
261
317
|
componentVersionRange ? `@${componentVersionRange}` : "@latest"
|
|
262
318
|
}/${this.component.name}/${this.component.name}.js?id=${id}`;
|
|
@@ -333,6 +389,28 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
333
389
|
this.log.error(e);
|
|
334
390
|
}
|
|
335
391
|
};
|
|
392
|
+
|
|
393
|
+
injectViteScripts = () => {
|
|
394
|
+
if (document.querySelector('script[name="ccc-sdk-react-preamble"]')) return;
|
|
395
|
+
|
|
396
|
+
const preambleScript = document.createElement("script");
|
|
397
|
+
preambleScript.type = "module";
|
|
398
|
+
preambleScript.setAttribute("name", "ccc-sdk-react-preamble");
|
|
399
|
+
|
|
400
|
+
preambleScript.textContent = `
|
|
401
|
+
import RefreshRuntime from "${this.testUrl?.origin}/@react-refresh";
|
|
402
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
403
|
+
window.$RefreshReg$ = () => {};
|
|
404
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
405
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
406
|
+
`.trim();
|
|
407
|
+
document.head.appendChild(preambleScript);
|
|
408
|
+
|
|
409
|
+
const viteClientScript = document.createElement("script");
|
|
410
|
+
viteClientScript.type = "module";
|
|
411
|
+
viteClientScript.src = `${this.testUrl?.origin}/@vite/client`;
|
|
412
|
+
document.head.appendChild(viteClientScript);
|
|
413
|
+
};
|
|
336
414
|
}
|
|
337
415
|
|
|
338
416
|
export interface CustomCodeComponent extends ContentTree.Node {
|
package/src/events.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { ComponentPath } from "./path";
|
|
2
2
|
|
|
3
|
-
export class
|
|
4
|
-
static eventType = "ccc:connected";
|
|
5
|
-
|
|
3
|
+
export class CCCEvent extends Event {
|
|
6
4
|
component: ComponentPath;
|
|
7
5
|
source?: string;
|
|
6
|
+
static eventType = "ccc:event";
|
|
8
7
|
|
|
9
8
|
constructor(
|
|
9
|
+
eventType: string = CCCEvent.eventType,
|
|
10
10
|
detail: { component: ComponentPath; source?: string },
|
|
11
11
|
opts?: EventInit
|
|
12
12
|
) {
|
|
13
|
-
super(
|
|
13
|
+
super(eventType, {
|
|
14
14
|
bubbles: true,
|
|
15
15
|
cancelable: false,
|
|
16
16
|
composed: true,
|
|
@@ -20,3 +20,45 @@ export class CCCConnectedEvent extends Event {
|
|
|
20
20
|
this.source = detail.source;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
export class CCCConnectedEvent extends CCCEvent {
|
|
25
|
+
static eventType = "ccc:connected";
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
detail: { component: ComponentPath; source?: string },
|
|
29
|
+
opts?: EventInit
|
|
30
|
+
) {
|
|
31
|
+
super(CCCConnectedEvent.eventType, detail, opts);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class CCCReadyEvent extends CCCEvent {
|
|
36
|
+
static eventType = "ccc:ready";
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
detail: { component: ComponentPath; source?: string },
|
|
40
|
+
opts?: EventInit
|
|
41
|
+
) {
|
|
42
|
+
super(CCCReadyEvent.eventType, detail, opts);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class CCCViewportEvent extends CCCEvent {
|
|
47
|
+
static eventType = "ccc:viewport";
|
|
48
|
+
intersecting = false;
|
|
49
|
+
entry?: IntersectionObserverEntry;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
detail: {
|
|
53
|
+
component: ComponentPath;
|
|
54
|
+
source?: string;
|
|
55
|
+
intersecting: boolean;
|
|
56
|
+
entry?: IntersectionObserverEntry;
|
|
57
|
+
},
|
|
58
|
+
opts?: EventInit
|
|
59
|
+
) {
|
|
60
|
+
super(CCCViewportEvent.eventType, detail, opts);
|
|
61
|
+
this.intersecting = detail.intersecting;
|
|
62
|
+
this.entry = detail.entry;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/path.ts
CHANGED
|
@@ -12,7 +12,7 @@ export class ComponentPath {
|
|
|
12
12
|
name: string;
|
|
13
13
|
versionRange: string;
|
|
14
14
|
|
|
15
|
-
constructor(path
|
|
15
|
+
constructor(path?: ComponentPathType | string) {
|
|
16
16
|
const { org, repo, name, versionRange } = isValidComponentPathObject(path)
|
|
17
17
|
? path
|
|
18
18
|
: ComponentPath.fromString(path);
|
|
@@ -36,12 +36,18 @@ export class ComponentPath {
|
|
|
36
36
|
return `${this.org}/${this.repo}@${this.versionRange}/${this.name}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
get isValid(): boolean {
|
|
40
|
+
return [this.org, this.repo, this.name].every(
|
|
41
|
+
(value) => value !== "unknown"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
toString(): string {
|
|
40
46
|
return this.path;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
static fromString(
|
|
44
|
-
|
|
49
|
+
static fromString(p?: string | null, v?: string | null): ComponentPath {
|
|
50
|
+
const path = p ?? "unknown/unknown/unknown";
|
|
45
51
|
|
|
46
52
|
const [name, repo, org] = path
|
|
47
53
|
.replace(/@[^\/]+/, "")
|