@financial-times/custom-code-component 2.0.13 → 2.0.15
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 +115 -2
- package/dist/custom-element.js +214 -126
- package/dist/custom-element.js.map +1 -1
- package/package.json +1 -1
- package/src/custom-code-component.ts +157 -9
- package/src/logger.ts +20 -6
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { ContentTree } from "@financial-times/content-tree";
|
|
7
7
|
import { BaseRenderer } from "../../ccc-sdk/src/renderers/BaseRenderer";
|
|
8
8
|
import Tracking from "./tracking";
|
|
9
|
-
import {
|
|
9
|
+
import { Logger } from "./logger";
|
|
10
10
|
import {
|
|
11
11
|
CCCError,
|
|
12
12
|
CCCImportError,
|
|
@@ -20,8 +20,20 @@ import { assignTestURL, useComponentTestEnv } from "./environment";
|
|
|
20
20
|
import styles from "./custom-code-component.css?inline";
|
|
21
21
|
|
|
22
22
|
export class FTCustomCodeComponent extends HTMLElement {
|
|
23
|
+
/**
|
|
24
|
+
* The component renderer, resolved from dynamic import
|
|
25
|
+
*/
|
|
23
26
|
app?: typeof BaseRenderer.prototype.render;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set whether component is "open" or "closed". Defaults to "open".
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#mode
|
|
31
|
+
*/
|
|
24
32
|
mode: "closed" | "open" = "open";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reserved attributes we don't pass to the child component
|
|
36
|
+
*/
|
|
25
37
|
RESERVED_ATTRS = new Set([
|
|
26
38
|
"iframe",
|
|
27
39
|
"path",
|
|
@@ -33,14 +45,36 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
33
45
|
"load-timeout",
|
|
34
46
|
]);
|
|
35
47
|
|
|
48
|
+
/**
|
|
49
|
+
* URI of component module for CCC webcomponent to load
|
|
50
|
+
*/
|
|
36
51
|
source?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* OTracking config
|
|
55
|
+
*/
|
|
37
56
|
tracking?: Tracking;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Alternative base URL, used for testing
|
|
60
|
+
*/
|
|
38
61
|
testUrl?: URL;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* ComponentPath instance with `org`, `repo`, `name` and `versionRange` attributes
|
|
65
|
+
*/
|
|
39
66
|
component = new ComponentPath();
|
|
40
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Logger instance. Set log level using `this.log.setLogLevel(string|number|null)`.
|
|
70
|
+
*/
|
|
41
71
|
log: Logger;
|
|
42
72
|
|
|
73
|
+
/**
|
|
74
|
+
* IntersectionObserver that dispatches CCCViewportEvents
|
|
75
|
+
*/
|
|
43
76
|
observer = new IntersectionObserver((entries) => {
|
|
77
|
+
this.log.debug("Intersection Observer callback", { entries });
|
|
44
78
|
entries.forEach((entry) => {
|
|
45
79
|
this.dispatchEvent(
|
|
46
80
|
new CCCViewportEvent({
|
|
@@ -53,11 +87,15 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
53
87
|
});
|
|
54
88
|
});
|
|
55
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Custom Element constructor.
|
|
92
|
+
*
|
|
93
|
+
* n.b., attributes and ShadowDOM aren't available in custom element constructors.
|
|
94
|
+
* Use connectedCallback() and other lifecycle methods if you want to eg. this.getAttribute()
|
|
95
|
+
*/
|
|
56
96
|
constructor() {
|
|
57
97
|
super();
|
|
58
|
-
this.log = new Logger(
|
|
59
|
-
level: convertStringLogLevel(this.getAttribute("log")),
|
|
60
|
-
});
|
|
98
|
+
this.log = new Logger();
|
|
61
99
|
|
|
62
100
|
const supportsDeclarative =
|
|
63
101
|
HTMLElement.prototype.hasOwnProperty("attachInternals");
|
|
@@ -69,8 +107,16 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
69
107
|
}
|
|
70
108
|
}
|
|
71
109
|
|
|
110
|
+
/**
|
|
111
|
+
* connectedCallback() CE lifecycle callback
|
|
112
|
+
*
|
|
113
|
+
* Called whenever a <custom-code-component> element is mounted to the DOM.
|
|
114
|
+
*/
|
|
72
115
|
async connectedCallback() {
|
|
73
116
|
try {
|
|
117
|
+
this.log.setLogLevel(this.getAttribute("log"));
|
|
118
|
+
this.log.debug("connectedCallback");
|
|
119
|
+
|
|
74
120
|
const path = this.getAttribute("path");
|
|
75
121
|
const versionRange = this.getAttribute("version");
|
|
76
122
|
this.component = ComponentPath.fromString(path, versionRange);
|
|
@@ -89,8 +135,13 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
89
135
|
}
|
|
90
136
|
}
|
|
91
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Error handler. Decorates errors for easier ingestion.
|
|
140
|
+
*
|
|
141
|
+
* @param error
|
|
142
|
+
*/
|
|
92
143
|
emitError(error: Error) {
|
|
93
|
-
|
|
144
|
+
this.log.debug("emitError", { error });
|
|
94
145
|
if (error instanceof CCCError) {
|
|
95
146
|
this.dispatchEvent(
|
|
96
147
|
new ErrorEvent("ccc:error", {
|
|
@@ -120,17 +171,42 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
120
171
|
}
|
|
121
172
|
}
|
|
122
173
|
|
|
174
|
+
/**
|
|
175
|
+
* disconnectedCallback() CE lifecycle event.
|
|
176
|
+
*
|
|
177
|
+
* Called when a <custom-code-component> element is removed from DOM.
|
|
178
|
+
*/
|
|
123
179
|
disconnectedCallback() {
|
|
180
|
+
this.log.debug("disconnectedCallback");
|
|
124
181
|
const path = this.getAttribute("path");
|
|
125
182
|
this.log.info(`<custom-code-component:${path}> disconnected`);
|
|
126
183
|
this.observer.disconnect();
|
|
127
184
|
}
|
|
128
185
|
|
|
186
|
+
/**
|
|
187
|
+
* MessageChannel interface.
|
|
188
|
+
*
|
|
189
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
|
|
190
|
+
*/
|
|
129
191
|
channel = new MessageChannel();
|
|
130
192
|
|
|
131
|
-
|
|
193
|
+
/**
|
|
194
|
+
* MessageChannel postMessage callback
|
|
195
|
+
*
|
|
196
|
+
* @param e
|
|
197
|
+
*/
|
|
198
|
+
onmessage(e?: any) {
|
|
199
|
+
this.log.debug("onmessage", { event: e });
|
|
200
|
+
}
|
|
132
201
|
|
|
202
|
+
/**
|
|
203
|
+
* This error handler is called by the child component on e.g. unrecoverable error.
|
|
204
|
+
* It then calls unmount(), which restores the fallback.
|
|
205
|
+
*
|
|
206
|
+
* @param e
|
|
207
|
+
*/
|
|
133
208
|
onunmount(e: Error) {
|
|
209
|
+
this.log.debug("onunmount", { error: e });
|
|
134
210
|
if (e instanceof Error) {
|
|
135
211
|
const renderError = new CCCRenderError(e.message, {
|
|
136
212
|
error: e,
|
|
@@ -143,9 +219,16 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
143
219
|
}
|
|
144
220
|
}
|
|
145
221
|
|
|
222
|
+
/**
|
|
223
|
+
* onready event callback.
|
|
224
|
+
* Called by mount() after kicking off initial component render.
|
|
225
|
+
*
|
|
226
|
+
* @param app
|
|
227
|
+
*/
|
|
146
228
|
async onready(app: Promise<void>) {
|
|
147
229
|
try {
|
|
148
230
|
await app;
|
|
231
|
+
this.log.debug("onready", { app });
|
|
149
232
|
this.dispatchEvent(
|
|
150
233
|
new CCCReadyEvent({
|
|
151
234
|
component: this.component,
|
|
@@ -158,6 +241,7 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
158
241
|
|
|
159
242
|
this.observer.observe(this);
|
|
160
243
|
} catch (e) {
|
|
244
|
+
this.log.debug("onready caught error", { error: e });
|
|
161
245
|
if (e instanceof Error) {
|
|
162
246
|
const renderError = new CCCRenderError(e.message, {
|
|
163
247
|
error: e,
|
|
@@ -171,11 +255,27 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
171
255
|
}
|
|
172
256
|
}
|
|
173
257
|
|
|
258
|
+
/**
|
|
259
|
+
* MessageChannel postMessage handler.
|
|
260
|
+
* This can be used for inter-CCC communication.
|
|
261
|
+
* @param event
|
|
262
|
+
*/
|
|
174
263
|
postMessage(event: any) {
|
|
264
|
+
this.log.debug("postmessage", { event });
|
|
175
265
|
this.channel.port1.postMessage(event);
|
|
176
266
|
}
|
|
177
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Initial mounting behaviour.
|
|
270
|
+
*
|
|
271
|
+
* Attaches ShadowDOM if needed, dispatches ccc:connected event, adds component base styles,
|
|
272
|
+
* generates fallback template element if one doesn't exist already, initialises tracking,
|
|
273
|
+
* then kicks off component rendering. Passes component render promise to onready().
|
|
274
|
+
*
|
|
275
|
+
* @param prerendered
|
|
276
|
+
*/
|
|
178
277
|
async mount(prerendered?: ShadowRoot | null) {
|
|
278
|
+
this.log.debug("mount", prerendered);
|
|
179
279
|
try {
|
|
180
280
|
this.mode =
|
|
181
281
|
this.getAttribute("shadow-open") == "false" ? "closed" : "open";
|
|
@@ -200,6 +300,7 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
200
300
|
!shadow.adoptedStyleSheets.length
|
|
201
301
|
) {
|
|
202
302
|
if (!window.CCC_LAYOUT_STYLESHEET) {
|
|
303
|
+
this.log.debug("mount generating CCC_LAYOUT_STYLESHEET");
|
|
203
304
|
window.CCC_LAYOUT_STYLESHEET = new CSSStyleSheet();
|
|
204
305
|
window.CCC_LAYOUT_STYLESHEET.replaceSync(styles);
|
|
205
306
|
}
|
|
@@ -207,6 +308,7 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
207
308
|
}
|
|
208
309
|
|
|
209
310
|
if (!ssr) {
|
|
311
|
+
this.log.debug("mount generating fallback template");
|
|
210
312
|
const template = document.createElement("template");
|
|
211
313
|
template.innerHTML = "<div data-component-root><slot></slot></div>";
|
|
212
314
|
this.appendChild(template);
|
|
@@ -252,6 +354,7 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
252
354
|
shadow,
|
|
253
355
|
{
|
|
254
356
|
...(data ?? extraProps),
|
|
357
|
+
log: this.log,
|
|
255
358
|
data: data ?? extraProps,
|
|
256
359
|
port: this.channel.port2,
|
|
257
360
|
tracking: this.tracking,
|
|
@@ -276,16 +379,23 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
276
379
|
}
|
|
277
380
|
}
|
|
278
381
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Called in top-level error handler or by child component on unhandled error.
|
|
384
|
+
* Replace shadow root with either <slot> or template[data-component-fallback]
|
|
385
|
+
* slot on failure
|
|
386
|
+
*
|
|
387
|
+
* @param e
|
|
388
|
+
*/
|
|
282
389
|
unmount(e: Error) {
|
|
390
|
+
this.log.debug("unmount", { error: e });
|
|
391
|
+
|
|
283
392
|
const template =
|
|
284
393
|
this.querySelector<HTMLTemplateElement>(
|
|
285
394
|
"template[data-component-fallback]"
|
|
286
395
|
) ?? this.querySelector<HTMLTemplateElement>("template");
|
|
287
396
|
|
|
288
397
|
if (template) {
|
|
398
|
+
this.log.debug("unmount replacing shadowRoot with fallback");
|
|
289
399
|
this.shadowRoot?.replaceChildren(template.content.cloneNode(true));
|
|
290
400
|
}
|
|
291
401
|
|
|
@@ -297,10 +407,18 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
297
407
|
this.observer.disconnect();
|
|
298
408
|
}
|
|
299
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Asynchronously loads the CCC child component.
|
|
412
|
+
*
|
|
413
|
+
* @returns Promise resolving to component renderer function
|
|
414
|
+
*/
|
|
300
415
|
async load() {
|
|
416
|
+
this.log.debug("load");
|
|
417
|
+
|
|
301
418
|
if (!this.component.isValid) {
|
|
302
419
|
throw new Error("No path found");
|
|
303
420
|
}
|
|
421
|
+
|
|
304
422
|
const path = this.getAttribute("path");
|
|
305
423
|
const componentVersionRange = this.getAttribute("version");
|
|
306
424
|
const timeout = Number(this.getAttribute("load-timeout") || 10000);
|
|
@@ -315,6 +433,7 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
315
433
|
const id = this.getAttribute("id");
|
|
316
434
|
|
|
317
435
|
if (isTestEnv && this.testUrl) {
|
|
436
|
+
this.log.debug("load adding Vite scripts");
|
|
318
437
|
this.injectViteScripts();
|
|
319
438
|
}
|
|
320
439
|
|
|
@@ -384,7 +503,11 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
384
503
|
}
|
|
385
504
|
}
|
|
386
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Initialises OTracking
|
|
508
|
+
*/
|
|
387
509
|
initTracking = async () => {
|
|
510
|
+
this.log.debug("initTracking", { cccId: this.id });
|
|
388
511
|
try {
|
|
389
512
|
this.tracking?.init(this.id);
|
|
390
513
|
} catch (e) {
|
|
@@ -397,6 +520,9 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
397
520
|
}
|
|
398
521
|
};
|
|
399
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Injects Vite HMR scripts during dev environment usage
|
|
525
|
+
*/
|
|
400
526
|
injectViteScripts = () => {
|
|
401
527
|
if (document.querySelector('script[name="ccc-sdk-react-preamble"]')) return;
|
|
402
528
|
|
|
@@ -418,6 +544,28 @@ export class FTCustomCodeComponent extends HTMLElement {
|
|
|
418
544
|
viteClientScript.src = `${this.testUrl?.origin}/@vite/client`;
|
|
419
545
|
document.head.appendChild(viteClientScript);
|
|
420
546
|
};
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Called whenever the <custom-code-component>'s attributes are changed.
|
|
550
|
+
* Currently only used to dynamically set log level.
|
|
551
|
+
*
|
|
552
|
+
* @param name
|
|
553
|
+
* @param oldValue
|
|
554
|
+
* @param newValue
|
|
555
|
+
*/
|
|
556
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
557
|
+
this.log.debug("attributeChangedCallback lifecycle method", {
|
|
558
|
+
name,
|
|
559
|
+
oldValue,
|
|
560
|
+
newValue,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
switch (name) {
|
|
564
|
+
case "log": {
|
|
565
|
+
this.log.setLogLevel(newValue);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
421
569
|
}
|
|
422
570
|
|
|
423
571
|
export interface CustomCodeComponent extends ContentTree.Node {
|
package/src/logger.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const LogLevel = Object.freeze({
|
|
|
7
7
|
DEFAULT: 2,
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
+
const LOG_PREFIX = "CCC:";
|
|
11
|
+
|
|
10
12
|
export function convertStringLogLevel(value: string | null) {
|
|
11
13
|
const level = value?.toLowerCase();
|
|
12
14
|
|
|
@@ -34,18 +36,20 @@ export function convertStringLogLevel(value: string | null) {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export class Logger {
|
|
37
|
-
level: number;
|
|
39
|
+
public level: number;
|
|
38
40
|
constructor(
|
|
39
41
|
{ level = LogLevel.DEFAULT }: { level: number } = {
|
|
40
42
|
level: LogLevel.DEFAULT,
|
|
41
43
|
}
|
|
42
44
|
) {
|
|
43
|
-
this.level =
|
|
45
|
+
this.level = localStorage.getItem("CCC_LOG_LEVEL")
|
|
46
|
+
? convertStringLogLevel(localStorage.getItem("CCC_LOG_LEVEL"))
|
|
47
|
+
: level;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
debug(...args: any[]) {
|
|
47
51
|
if (this.level <= LogLevel.DEBUG) {
|
|
48
|
-
console.info(...args);
|
|
52
|
+
console.info(LOG_PREFIX, ...args);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -53,19 +57,29 @@ export class Logger {
|
|
|
53
57
|
|
|
54
58
|
info(...args: any[]) {
|
|
55
59
|
if (this.level <= LogLevel.INFO) {
|
|
56
|
-
console.info(...args);
|
|
60
|
+
console.info(LOG_PREFIX, ...args);
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
warn(...args: any[]) {
|
|
61
65
|
if (this.level <= LogLevel.WARN) {
|
|
62
|
-
console.warn(...args);
|
|
66
|
+
console.warn(LOG_PREFIX, ...args);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
error(...args: any[]) {
|
|
67
71
|
if (this.level <= LogLevel.ERROR) {
|
|
68
|
-
console.error(...args);
|
|
72
|
+
console.error(LOG_PREFIX, ...args);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setLogLevel(level: string | number | null) {
|
|
77
|
+
if (localStorage.getItem("CCC_LOG_LEVEL")) {
|
|
78
|
+
this.level = convertStringLogLevel(localStorage.getItem("CCC_LOG_LEVEL"));
|
|
79
|
+
} else if (typeof level === "number") {
|
|
80
|
+
this.level = level;
|
|
81
|
+
} else {
|
|
82
|
+
this.level = convertStringLogLevel(level);
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
}
|