@financial-times/custom-code-component 1.7.1 → 1.8.0

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.
@@ -1,4 +1,4 @@
1
- class f extends HTMLElement {
1
+ class l extends HTMLElement {
2
2
  constructor() {
3
3
  super(...arguments), this.RESERVED_ATTRS = /* @__PURE__ */ new Set([
4
4
  "iframe",
@@ -7,86 +7,108 @@ class f extends HTMLElement {
7
7
  "data-component-props",
8
8
  "data-asset-type",
9
9
  "shadow-open",
10
- "env"
11
- ]), this.source = null, this.unmount = () => {
10
+ "env",
11
+ "load-timeout"
12
+ ]), this.unmount = (t) => {
12
13
  }, this.channel = new MessageChannel();
13
14
  }
14
15
  static get observedAttributes() {
15
16
  return ["path", "version", "env", "data-component-props"];
16
17
  }
17
18
  async mount() {
18
- var h, u, m;
19
- const e = this.getAttribute("path"), n = this.getAttribute("version"), i = (h = this.getAttribute("env")) == null ? void 0 : h.toLowerCase().startsWith("d");
20
- if (!e)
19
+ var c;
20
+ if (!this.app)
21
+ throw new Error("CCC mounted without App");
22
+ const t = this.shadowRoot ?? this.attachShadow({ mode: this.mode }), o = this.app, u = JSON.parse(this.getAttribute("data-component-props")), m = Object.fromEntries(
23
+ [...this.attributes].filter((r) => !this.RESERVED_ATTRS.has(r.name)).map((r) => [r.name, r.value])
24
+ );
25
+ (c = this.shadowRoot) == null || c.replaceChildren();
26
+ const { unmount: s, onmessage: n } = o(
27
+ t,
28
+ { ...m, data: u, port: this.channel.port2 },
29
+ ...this.children
30
+ ) || {};
31
+ s && (this.unmount = s), n && (this.onmessage = n);
32
+ }
33
+ async connectedCallback() {
34
+ var r;
35
+ const t = this.getAttribute("path"), o = this.getAttribute("version"), u = this.getAttribute("load-timeout") ?? 2e3, m = (r = this.getAttribute("env")) == null ? void 0 : r.toLowerCase().startsWith("d");
36
+ if (!t)
21
37
  throw new Error(
22
38
  "path attribute not specified in <custom-code-component>"
23
39
  );
24
- const [s, a, r] = e.split("/").reverse();
25
- if (!(!s || !a || !r)) {
26
- this.source = i ? `http://localhost:5173/src/${s}/index.jsx` : `https://www.ft.com/__component/${r}/${a}${n ? `@${n}` : "@latest"}/${s}/${s}.js`;
40
+ const [s, n, c] = t.split("/").reverse();
41
+ if (!(!s || !n || !c)) {
42
+ this.source = m ? `http://localhost:5173/src/${s}/index.jsx` : `https://www.ft.com/__component/${c}/${n}${o ? `@${o}` : "@latest"}/${s}/${s}.js`;
43
+ try {
44
+ this.app = await new Promise((e, p) => {
45
+ const d = setTimeout(
46
+ () => p(new i("CCC import timeout")),
47
+ Number(u)
48
+ );
49
+ import(
50
+ /* webpackIgnore: true */
51
+ this.source
52
+ /* @vite-ignore */
53
+ ).then(({ default: a }) => {
54
+ if (a)
55
+ clearTimeout(d), e(a);
56
+ else
57
+ throw new h(
58
+ "No component renderer default export found"
59
+ );
60
+ }).catch((a) => p(new h(a)));
61
+ });
62
+ } catch (e) {
63
+ console.error(
64
+ `<custom-code-component> error during import from ${t}@${o}`
65
+ ), delete this.dataset.cccReady, e instanceof i ? this.dataset.cccError = "import-timeout" : this.dataset.cccError = "import-failure", this.dispatchEvent(new ErrorEvent("error", e)), console.error(e);
66
+ return;
67
+ }
27
68
  try {
28
- const c = await import(
29
- /* webpackIgnore: true */
30
- this.source
31
- /* @vite-ignore */
32
- ), p = JSON.parse(this.getAttribute("data-component-props")), d = Object.fromEntries(
33
- [...this.attributes].filter((t) => !this.RESERVED_ATTRS.has(t.name)).map((t) => [t.name, t.value])
34
- );
35
- if ((u = this.shadowRoot) == null || u.replaceChildren(), this.hasAttribute("iframe")) {
36
- const t = document.createElement("iframe");
37
- t.addEventListener("load", () => {
38
- const { unmount: o, onmessage: l } = c.default(
39
- t.contentDocument,
40
- { ...d, data: p, port: this.channel.port2 },
41
- ...this.children
42
- ) || {};
43
- o && (this.unmount = o), l && (this.onmessage = l);
44
- }), (m = this.shadowRoot) == null || m.append(t);
45
- } else {
46
- const { unmount: t, onmessage: o } = await c.default(
47
- this.shadowRoot,
48
- { ...d, data: p, port: this.channel.port2 },
49
- ...this.children
50
- ) || {};
51
- t && (this.unmount = t), o && (this.onmessage = o);
52
- }
53
- } catch (c) {
54
- console.info(`<custom-code-component> uncaught error from ${e}`), console.error(c);
69
+ this.mode = this.getAttribute("shadow-open") == "false" ? "closed" : "open", await this.mount(), this.dispatchEvent(
70
+ new CustomEvent("ccc-connected", {
71
+ bubbles: !0,
72
+ cancelable: !0,
73
+ detail: {
74
+ component: `${t}@${o}`,
75
+ source: this.source
76
+ }
77
+ })
78
+ ), this.dataset.cccReady = "true", delete this.dataset.cccError;
79
+ } catch (e) {
80
+ console.info(
81
+ `<custom-code-component> uncaught error during mount from ${t}@${o}`
82
+ ), console.error(e), this.dispatchEvent(new ErrorEvent("error", e)), this.dataset.cccError = "mount-error", delete this.dataset.cccReady;
55
83
  }
56
84
  }
57
85
  }
58
- connectedCallback() {
59
- const e = this.getAttribute("shadow-open") == "false" ? "closed" : "open";
60
- this.attachShadow({ mode: e }), this.mount().then(() => {
61
- const n = this.getAttribute("path"), i = this.getAttribute("version");
62
- this.dispatchEvent(
63
- new CustomEvent("ccc-connected", {
64
- bubbles: !0,
65
- cancelable: !0,
66
- detail: {
67
- component: `${n}@${i}`,
68
- source: this.source
69
- }
70
- })
71
- );
72
- });
73
- }
74
86
  attributeChangedCallback() {
75
87
  this.mount();
76
88
  }
77
89
  disconnectedCallback() {
78
- const e = this.getAttribute("path");
79
- console.info(`<custom-code-component:${e}> disconnected`), typeof this.unmount == "function" && this.unmount();
90
+ const t = this.getAttribute("path");
91
+ console.info(`<custom-code-component:${t}> disconnected`), typeof this.unmount == "function" && this.unmount();
80
92
  }
81
93
  onmessage() {
82
94
  }
83
95
  // I'm honestly not sure what to do with this
84
- postMessage(e) {
85
- this.channel.port1.postMessage(e);
96
+ postMessage(t) {
97
+ this.channel.port1.postMessage(t);
98
+ }
99
+ }
100
+ const E = () => customElements.define("custom-code-component", l);
101
+ customElements && !customElements.get("custom-code-component") && E();
102
+ class i extends Error {
103
+ constructor(...t) {
104
+ super(...t), Error.captureStackTrace && Error.captureStackTrace(this, i), this.name = "CCCImportTimeoutError";
105
+ }
106
+ }
107
+ class h extends Error {
108
+ constructor(...t) {
109
+ super(...t), Error.captureStackTrace && Error.captureStackTrace(this, h), this.name = "CCCImportError";
86
110
  }
87
111
  }
88
- const g = () => customElements.define("custom-code-component", f);
89
- customElements && !customElements.get("custom-code-component") && g();
90
112
  export {
91
- g as init
113
+ E as init
92
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/custom-code-component",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -4,8 +4,12 @@
4
4
  */
5
5
 
6
6
  import { ContentTree } from "@financial-times/content-tree";
7
-
7
+ import { BaseRenderer } from "../../ccc-sdk/src/renderers/AbstractBaseRenderer";
8
8
  class FTCustomCodeComponent extends HTMLElement {
9
+ app: typeof BaseRenderer.prototype.render;
10
+
11
+ mode: "closed" | "open";
12
+
9
13
  RESERVED_ATTRS = new Set([
10
14
  "iframe",
11
15
  "path",
@@ -14,17 +18,52 @@ class FTCustomCodeComponent extends HTMLElement {
14
18
  "data-asset-type",
15
19
  "shadow-open",
16
20
  "env",
21
+ "load-timeout",
17
22
  ]);
18
23
 
19
24
  static get observedAttributes() {
20
25
  return ["path", "version", "env", "data-component-props"];
21
26
  }
22
27
 
23
- source: string | null = null;
28
+ source: string;
24
29
 
25
30
  async mount() {
31
+ if (!this.app) {
32
+ throw new Error("CCC mounted without App");
33
+ }
34
+
35
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: this.mode });
36
+
37
+ const App = this.app;
38
+
39
+ const data = JSON.parse(this.getAttribute("data-component-props")!);
40
+
41
+ const extraProps = Object.fromEntries(
42
+ [...this.attributes]
43
+ .filter((attribute) => !this.RESERVED_ATTRS.has(attribute.name))
44
+ .map((attribute) => [attribute.name, attribute.value])
45
+ );
46
+
47
+ // Clear old children
48
+ this.shadowRoot?.replaceChildren();
49
+
50
+ const { unmount, onmessage } =
51
+ App(
52
+ shadow,
53
+ { ...extraProps, data, port: this.channel.port2 },
54
+ ...this.children
55
+ ) || {};
56
+
57
+ if (unmount) this.unmount = unmount;
58
+ if (onmessage) this.onmessage = onmessage;
59
+ }
60
+
61
+ unmount = (root?: any) => {};
62
+
63
+ async connectedCallback() {
26
64
  const path = this.getAttribute("path");
27
65
  const componentVersionRange = this.getAttribute("version");
66
+ const timeout = this.getAttribute("load-timeout") ?? 2000;
28
67
  const useLocalVersion = this.getAttribute("env")
29
68
  ?.toLowerCase()
30
69
  .startsWith("d");
@@ -47,64 +86,48 @@ class FTCustomCodeComponent extends HTMLElement {
47
86
  }/${componentName}/${componentName}.js`;
48
87
 
49
88
  try {
50
- const App = await import(
51
- /* webpackIgnore: true */ this.source /* @vite-ignore */
52
- );
53
-
54
- const data = JSON.parse(this.getAttribute("data-component-props")!);
55
-
56
- const extraProps = Object.fromEntries(
57
- [...this.attributes]
58
- .filter((attribute) => !this.RESERVED_ATTRS.has(attribute.name))
59
- .map((attribute) => [attribute.name, attribute.value])
89
+ this.app = await new Promise((resolve, reject) => {
90
+ const to = setTimeout(
91
+ () => reject(new CCCTimeoutError("CCC import timeout")),
92
+ Number(timeout)
93
+ );
94
+ import(/* webpackIgnore: true */ this.source /* @vite-ignore */)
95
+ .then(({ default: componentRenderFunction }) => {
96
+ if (componentRenderFunction) {
97
+ clearTimeout(to);
98
+ resolve(componentRenderFunction);
99
+ } else
100
+ throw new CCCImportError(
101
+ "No component renderer default export found"
102
+ );
103
+ })
104
+ .catch((e) => reject(new CCCImportError(e)));
105
+ });
106
+ } catch (e) {
107
+ console.error(
108
+ `<custom-code-component> error during import from ${path}@${componentVersionRange}`
60
109
  );
61
110
 
62
- // Clear old children
63
- this.shadowRoot?.replaceChildren();
111
+ delete this.dataset.cccReady;
64
112
 
65
- if (this.hasAttribute("iframe")) {
66
- const mountPoint = document.createElement("iframe");
67
-
68
- mountPoint.addEventListener("load", () => {
69
- const { unmount, onmessage } =
70
- App.default(
71
- mountPoint.contentDocument,
72
- { ...extraProps, data, port: this.channel.port2 },
73
- ...this.children
74
- ) || {};
75
-
76
- if (unmount) this.unmount = unmount;
77
- if (onmessage) this.onmessage = onmessage;
78
- });
79
-
80
- this.shadowRoot?.append(mountPoint);
113
+ if (e instanceof CCCTimeoutError) {
114
+ this.dataset.cccError = "import-timeout";
81
115
  } else {
82
- const { unmount, onmessage } =
83
- (await App.default(
84
- this.shadowRoot,
85
- { ...extraProps, data, port: this.channel.port2 },
86
- ...this.children
87
- )) || {};
88
-
89
- if (unmount) this.unmount = unmount;
90
- if (onmessage) this.onmessage = onmessage;
116
+ this.dataset.cccError = "import-failure";
91
117
  }
92
- } catch (e) {
93
- console.info(`<custom-code-component> uncaught error from ${path}`);
118
+
119
+ this.dispatchEvent(new ErrorEvent("error", e));
94
120
  console.error(e);
121
+
122
+ return; // prevent mounting on caught error
95
123
  }
96
- }
97
124
 
98
- unmount = () => {};
125
+ try {
126
+ this.mode =
127
+ this.getAttribute("shadow-open") == "false" ? "closed" : "open";
99
128
 
100
- connectedCallback() {
101
- const mode =
102
- this.getAttribute("shadow-open") == "false" ? "closed" : "open";
129
+ await this.mount();
103
130
 
104
- this.attachShadow({ mode });
105
- this.mount().then(() => {
106
- const path = this.getAttribute("path");
107
- const componentVersionRange = this.getAttribute("version");
108
131
  this.dispatchEvent(
109
132
  new CustomEvent("ccc-connected", {
110
133
  bubbles: true,
@@ -115,7 +138,20 @@ class FTCustomCodeComponent extends HTMLElement {
115
138
  },
116
139
  })
117
140
  );
118
- });
141
+
142
+ this.dataset.cccReady = "true";
143
+ delete this.dataset.cccError;
144
+ } catch (e) {
145
+ console.info(
146
+ `<custom-code-component> uncaught error during mount from ${path}@${componentVersionRange}`
147
+ );
148
+ console.error(e);
149
+
150
+ this.dispatchEvent(new ErrorEvent("error", e));
151
+ this.dataset.cccError = "mount-error";
152
+
153
+ delete this.dataset.cccReady;
154
+ }
119
155
  }
120
156
 
121
157
  attributeChangedCallback() {
@@ -158,3 +194,31 @@ export interface CustomCodeComponent extends ContentTree.Node {
158
194
  [key: string]: string | boolean | undefined;
159
195
  } | { children?: CustomCodeComponent | Array<CustomCodeComponent> };
160
196
  }
197
+
198
+ class CCCTimeoutError extends Error {
199
+ constructor(...params) {
200
+ // Pass remaining arguments (including vendor specific ones) to parent constructor
201
+ super(...params);
202
+
203
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
204
+ if (Error.captureStackTrace) {
205
+ Error.captureStackTrace(this, CCCTimeoutError);
206
+ }
207
+
208
+ this.name = "CCCImportTimeoutError";
209
+ }
210
+ }
211
+
212
+ class CCCImportError extends Error {
213
+ constructor(...params) {
214
+ // Pass remaining arguments (including vendor specific ones) to parent constructor
215
+ super(...params);
216
+
217
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
218
+ if (Error.captureStackTrace) {
219
+ Error.captureStackTrace(this, CCCImportError);
220
+ }
221
+
222
+ this.name = "CCCImportError";
223
+ }
224
+ }