@financial-times/custom-code-component 0.0.1

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.
@@ -0,0 +1,44 @@
1
+ version: 2.1
2
+
3
+ orbs:
4
+ node: circleci/node@5.1.0
5
+ doppler-circleci: ft-circleci-orbs/doppler-circleci@1.3
6
+
7
+ references:
8
+ default_container_config: &default_container_config
9
+ executor:
10
+ name: node/default
11
+ tag: 'lts'
12
+
13
+ only_version_tags: &only_version_tags
14
+ tags:
15
+ only: /^v.*/
16
+ branches:
17
+ ignore: /.*/
18
+
19
+ jobs:
20
+ build:
21
+ <<: *default_container_config
22
+ steps:
23
+ - checkout
24
+ - doppler-circleci/install
25
+ - doppler-circleci/load_secrets:
26
+ doppler_token: DOPPLER_TOKEN_VS_COMPONENTS
27
+ - run:
28
+ name: Install dependencies
29
+ command: npm install
30
+ - run:
31
+ name: Publish package
32
+ command: |
33
+ npm set @Financial-Times:registry=https://npm.pkg.github.com/
34
+ npm set //npm.pkg.github.com/:_authToken=${GPR_WRITE_AUTH_TOKEN}
35
+ npm publish
36
+
37
+ workflows:
38
+ build-and-release:
39
+ jobs:
40
+ - build:
41
+ context:
42
+ - djd-starter-kit-s3-deployment # context is named the same in both orgs
43
+ filters:
44
+ <<: *only_version_tags
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist
package/.eslintrc.cjs ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es2021: true,
5
+ },
6
+ extends: ["eslint:recommended", "plugin:react/recommended"],
7
+ overrides: [
8
+ {
9
+ env: {
10
+ node: true,
11
+ },
12
+ files: [".eslintrc.{js,cjs}"],
13
+ parserOptions: {
14
+ sourceType: "script",
15
+ },
16
+ },
17
+ ],
18
+ parserOptions: {
19
+ ecmaVersion: "latest",
20
+ sourceType: "module",
21
+ },
22
+ rules: {},
23
+ };
@@ -0,0 +1 @@
1
+ * @Financial-Times/visual-data-journalism-admins
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # custom-code-component (`<custom-code-component>`)
2
+ ## Web component custom element for instantiating Visual & Data Journalism team projects
3
+
4
+ ## Usage:
5
+
6
+ Instantiate the web component and pass it a URL to a bundled component you wish to render into the component root.
7
+
8
+ #### example.jsx
9
+
10
+ ```jsx
11
+ import React from 'react';
12
+ import ReactDOM from 'react-dom';
13
+ import css from './Component.css?inline';
14
+
15
+ const App = (props) => <div>{JSON.stringify(props)}</div>;
16
+
17
+ export default (shadowRoot, props, ...children) => {
18
+ const style = document.createElement("style");
19
+ const mountPoint = document.createElement("div");
20
+
21
+ style.innerHTML = css;
22
+ mountPoint.id = "component-root";
23
+
24
+ shadowRoot.appendChild(style);
25
+ shadowRoot.appendChild(mountPoint);
26
+
27
+ ReactDOM.createRoot(mountPoint).render(
28
+ <React.StrictMode>
29
+ <App {...props}>{children}</App>
30
+ </React.StrictMode>
31
+ );
32
+ };
33
+
34
+ ```
35
+
36
+ #### index.html
37
+ ```html
38
+ <script src="custom-code-component.js" type="module"></script>
39
+
40
+ <custom-code-component path="ft-interactive/test-project/test-component" version="^1" data-component-props="{}" data-asset-type="custom-code-component">
41
+ <img alt="test component" src="https://ig.ft.com/components/ft-interactive/test-project/test-component@^1.png">
42
+ </custom-code-component>
43
+ ```
44
+
45
+ ## Attributes
46
+ * `path` (string)
47
+ * Component name in the format `[org]/[repo]/[component]`.
48
+ * If `[org]` is `ccc-sdk` or ommitted, component will be loaded from Vite local dev server (127.0.0.1:5173).
49
+ * Otherwise, it will be loaded from the CCCCDN
50
+ * `version` (string)
51
+ * [Semantic Versioning](https://semver.org) range for the component.
52
+ * `data-component-props` (string)
53
+ * Pass stringified JSON to make it available to children as the `data` prop.
54
+ * `iframe` (boolean)
55
+ * Render inside an iframe using `srcdoc` for extra sandboxing (default: false)
56
+ * `shadow-open` (boolean)
57
+ * Sets the shadow root to either open or closed (default: false)
58
+ * `data-asset-type="custom-code-component"`
59
+ * Part of spec.
60
+ * <any other attributes>
61
+ * All remaining attributes get passed as an object named `props` to render().
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @file
3
+ * Main component definition for g-react-loader
4
+ */
5
+ import { ContentTree } from "@financial-times/content-tree";
6
+ export declare const init: () => void;
7
+ export interface CustomCodeComponent extends ContentTree.Node {
8
+ type: "CustomCodeComponent";
9
+ path: string;
10
+ versionRange: string;
11
+ altText: string;
12
+ timestamp?: string;
13
+ fallbackImage?: ContentTree.Image;
14
+ attributes?: [string, string][];
15
+ }
@@ -0,0 +1,52 @@
1
+ class h extends HTMLElement {
2
+ constructor() {
3
+ super(...arguments), this.RESERVED_ATTRS = /* @__PURE__ */ new Set([
4
+ "iframe",
5
+ "path",
6
+ "version",
7
+ "data-component-props",
8
+ "data-asset-type",
9
+ "shadow-open"
10
+ ]);
11
+ }
12
+ async connectedCallback() {
13
+ const o = this.getAttribute("path"), n = this.hasAttribute("shadow-open") ? "open" : "closed", s = this.getAttribute("version");
14
+ if (!o)
15
+ throw new Error(
16
+ "path attribute not specified in <custom-code-component>"
17
+ );
18
+ const [c, a, i] = o.split("/").reverse(), p = !i || a === "ccc-sdk" ? `http://127.0.0.1:5173/src/${c}/index.jsx` : (
19
+ // @TODO create component service -- this URL may change!!!
20
+ `https://ig.ft.com/component/${i}/${a}/${c}${s ? `@${s}` : "@latest"}`
21
+ );
22
+ try {
23
+ const e = await import(
24
+ /* webpackIgnore: true */
25
+ p
26
+ /* @vite-ignore */
27
+ ), m = JSON.parse(this.getAttribute("data-component-props") || ""), r = Object.fromEntries(
28
+ [...this.attributes].filter((t) => !this.RESERVED_ATTRS.has(t.name)).map((t) => [t.name, t.value])
29
+ );
30
+ if (this.hasAttribute("iframe")) {
31
+ const t = document.createElement("iframe");
32
+ t.addEventListener("load", () => {
33
+ e.default(
34
+ t.contentDocument,
35
+ { ...r, data: m },
36
+ ...this.children
37
+ );
38
+ }), this.attachShadow({ mode: n }).append(t);
39
+ } else {
40
+ const t = this.attachShadow({ mode: n });
41
+ e.default(t, { ...r, data: m }, ...this.children);
42
+ }
43
+ } catch (e) {
44
+ console.info("<custom-code-component> caught: ", e);
45
+ }
46
+ }
47
+ }
48
+ const d = () => customElements.define("custom-code-component", h);
49
+ customElements && !customElements.get("custom-code-component") && d();
50
+ export {
51
+ d as init
52
+ };
package/index.html ADDED
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>custom-code-component loader</title>
7
+ </head>
8
+ <body>
9
+ <custom-code-component
10
+ src="../test/test16.jsx"
11
+ data='{"jsonWorks": true}'
12
+ extra="test 1"
13
+ ></custom-code-component>
14
+ <custom-code-component
15
+ src="../test/test18.jsx"
16
+ data="./test.json"
17
+ extra="test 2"
18
+ ></custom-code-component>
19
+
20
+ <script type="module" src="./src/custom-code-component.js"></script>
21
+ </body>
22
+ </html>
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@financial-times/custom-code-component",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/custom-code-component.js",
8
+ "types": "./dist/custom-code-component.d.ts"
9
+ }
10
+ },
11
+ "scripts": {
12
+ "dev": "vite",
13
+ "build": "vite build",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "devDependencies": {
17
+ "@financial-times/content-tree": "github:financial-times/content-tree",
18
+ "@vitejs/plugin-react": "^4.0.4",
19
+ "eslint": "^8.47.0",
20
+ "eslint-plugin-react": "^7.33.1",
21
+ "prettier": "^3.0.1",
22
+ "react-dom16": "npm:react-dom@^16.14.0",
23
+ "react16": "npm:react@^16.14.0",
24
+ "typescript": "^5.2.2",
25
+ "vite": "^4.4.9",
26
+ "vite-plugin-dts": "^3.6.0"
27
+ },
28
+ "dependencies": {
29
+ "react": "^18.2.0",
30
+ "react-dom": "^18.2.0"
31
+ },
32
+ "overrides": {
33
+ "react-dom16": {
34
+ "react": "react16"
35
+ },
36
+ "react16": {
37
+ "react-dom": "react-dom16"
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @file
3
+ * Main component definition for g-react-loader
4
+ */
5
+
6
+ import { ContentTree } from "@financial-times/content-tree";
7
+
8
+ class FTCustomCodeComponent extends HTMLElement {
9
+ RESERVED_ATTRS = new Set([
10
+ "iframe",
11
+ "path",
12
+ "version",
13
+ "data-component-props",
14
+ "data-asset-type",
15
+ "shadow-open",
16
+ ]);
17
+
18
+ async connectedCallback() {
19
+ const path = this.getAttribute("path");
20
+ const mode = this.hasAttribute("shadow-open") ? "open" : "closed";
21
+ const componentVersionRange = this.getAttribute("version");
22
+
23
+ if (!path)
24
+ throw new Error(
25
+ "path attribute not specified in <custom-code-component>"
26
+ );
27
+
28
+ const [componentName, componentRepo, componentOrg] = path
29
+ .split("/")
30
+ .reverse();
31
+
32
+ const source =
33
+ !componentOrg || componentRepo === "ccc-sdk"
34
+ ? `http://127.0.0.1:5173/src/${componentName}/index.jsx`
35
+ : // @TODO create component service -- this URL may change!!!
36
+ `https://ig.ft.com/component/${componentOrg}/${componentRepo}/${componentName}${
37
+ componentVersionRange ? `@${componentVersionRange}` : "@latest"
38
+ }`;
39
+ try {
40
+ const App = await import(
41
+ /* webpackIgnore: true */ source /* @vite-ignore */
42
+ );
43
+
44
+ const data = JSON.parse(this.getAttribute("data-component-props") || "");
45
+
46
+ const extraProps = Object.fromEntries(
47
+ [...this.attributes]
48
+ .filter((attribute) => !this.RESERVED_ATTRS.has(attribute.name))
49
+ .map((attribute) => [attribute.name, attribute.value])
50
+ );
51
+
52
+ if (this.hasAttribute("iframe")) {
53
+ const mountPoint = document.createElement("iframe");
54
+
55
+ mountPoint.addEventListener("load", () => {
56
+ App.default(
57
+ mountPoint.contentDocument,
58
+ { ...extraProps, data },
59
+ ...this.children
60
+ );
61
+ });
62
+
63
+ this.attachShadow({ mode }).append(mountPoint);
64
+ } else {
65
+ const shadowRoot = this.attachShadow({ mode });
66
+
67
+ App.default(shadowRoot, { ...extraProps, data }, ...this.children);
68
+ }
69
+ } catch (e) {
70
+ console.info("<custom-code-component> caught: ", e);
71
+ }
72
+ }
73
+ }
74
+
75
+ export const init = () =>
76
+ customElements.define("custom-code-component", FTCustomCodeComponent);
77
+
78
+ if (customElements && !customElements.get("custom-code-component")) init();
79
+
80
+ export interface CustomCodeComponent extends ContentTree.Node {
81
+ type: "CustomCodeComponent";
82
+ path: string;
83
+ versionRange: string;
84
+ altText: string;
85
+ timestamp?: string;
86
+ fallbackImage?: ContentTree.Image;
87
+ attributes?: [string, string][];
88
+ }
@@ -0,0 +1,6 @@
1
+ @import url("https://www.ft.com/__origami/service/build/v3/bundles/css?components=o-fonts@^5&brand=core&system_code=djd-projects");
2
+
3
+ :host {
4
+ background: 'transparent';
5
+ font-family: 'Metric', 'MetricWeb';
6
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react16";
2
+ import ReactDOM from "react-dom16";
3
+ import css from "./styles.css?inline";
4
+
5
+ // eslint-disable-next-line react/prop-types
6
+ const App = ({ children, ...props }) => (
7
+ <div>
8
+ <h1>React 16 component</h1>
9
+ {JSON.stringify(props)}
10
+ {children}
11
+ </div>
12
+ );
13
+
14
+ export default async (shadowRoot, props, ...children) => {
15
+ const style = document.createElement("style");
16
+ const mountPoint = document.createElement("div");
17
+
18
+ style.innerHTML = css;
19
+ mountPoint.id = "component-root";
20
+
21
+ shadowRoot.appendChild(style);
22
+ shadowRoot.appendChild(mountPoint);
23
+
24
+ // eslint-disable-next-line react/no-deprecated
25
+ ReactDOM.render(
26
+ <React.StrictMode>
27
+ <App {...props}>{children}</App>
28
+ </React.StrictMode>,
29
+ mountPoint
30
+ );
31
+ };
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import css from "./styles.css?inline";
4
+
5
+ // eslint-disable-next-line react/prop-types
6
+ const App = ({ children, ...props }) => (
7
+ <>
8
+ <h1>React 18 component</h1>
9
+ {JSON.stringify(props)}
10
+ {children}
11
+ </>
12
+ );
13
+
14
+ export default (shadowRoot, props, ...children) => {
15
+ const style = document.createElement("style");
16
+ const mountPoint = document.createElement("div");
17
+
18
+ style.innerHTML = css;
19
+ mountPoint.id = "component-root";
20
+
21
+ shadowRoot.appendChild(style);
22
+ shadowRoot.appendChild(mountPoint);
23
+
24
+ ReactDOM.createRoot(mountPoint).render(
25
+ <React.StrictMode>
26
+ <App {...props}>{children}</App>
27
+ </React.StrictMode>
28
+ );
29
+ };
package/test.json ADDED
@@ -0,0 +1 @@
1
+ { "hasJSON": true }
package/vite.config.js ADDED
@@ -0,0 +1,18 @@
1
+ // Vite.config.js
2
+ import { defineConfig } from "vite";
3
+ import react from "@vitejs/plugin-react";
4
+ import { basename } from "path";
5
+ import dts from "vite-plugin-dts";
6
+
7
+ export default defineConfig({
8
+ build: {
9
+ lib: {
10
+ // Could also be a dictionary or array of multiple entry points
11
+ entry: "./src/custom-code-component.ts",
12
+ formats: ["es"],
13
+ fileName: (format, entry) => `${basename(entry)}.js`,
14
+ name: "CustomCodeComponent",
15
+ },
16
+ },
17
+ plugins: [react(), dts()],
18
+ });