@financial-times/custom-code-component 2.0.14 → 2.0.16

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.
@@ -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 { convertStringLogLevel, Logger } from "./logger";
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
- console.debug("ccc#emitError", error);
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
- onmessage() {}
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
- // Called in top-level error handler
280
- // Replace shadow root with either <slot> or template[data-component-fallback]
281
- // slot on failure
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 = 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
  }