wc-compiler 0.1.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.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # wcc
2
+
3
+ [![Netlify Status](https://api.netlify.com/api/v1/badges/e718eac2-b3bc-4986-8569-49706a430beb/deploy-status)](https://app.netlify.com/sites/merry-caramel-524e61/deploys)
4
+ [![GitHub release](https://img.shields.io/github/tag/thescientist13/wcc.svg)](https://github.com/thescientist13/wcc/tags)
5
+ ![GitHub Actions status](https://github.com/thescientist13/wcc/workflows/Master%20Integration/badge.svg)
6
+ [![GitHub issues](https://img.shields.io/github/issues-pr-raw/thescientist13/wcc.svg)](https://github.com/thescientist13/wcc/issues)
7
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/thescientist13/wcc/master/LICENSE.md)
8
+
9
+ Experimental native Web Components compiler. (`<w⚙️⚙️/>`)
10
+
11
+ > _It's Web Components all the way down._ 🐢
12
+
13
+ ## Overview
14
+
15
+ **Web Components Compiler (WCC)** is a NodeJS package designed to make server-side rendering (SSR) of native Web Components easier. It can render (within reason 😅) your Web Component into static HTML leveraging [Declarative Shadow DOM](https://web.dev/declarative-shadow-dom/).
16
+
17
+ It is not a static site generator or framework. It is focused on producing raw HTML from Web Components with the intent of being easily _integrated_ into a site generator or framework.
18
+
19
+ > _The original motivation for this project was to create a [purpose built, lighter weight, alternative to puppeteer for SSR of `HTMLElement`](https://github.com/ProjectEvergreen/greenwood/issues/926) for the project [**Greenwood**](https://www.greenwoodjs.io/)._
20
+
21
+ In addition, WCC hopes to provide a surface area to explore patterns around [streaming](https://github.com/thescientist13/wcc/issues/5) and serverless rendering, as well as acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg) discussions around community protocols, like [hydration](https://github.com/thescientist13/wcc/issues/3).
22
+
23
+ ## Key Features
24
+
25
+ 1. Supports the following `HTMLElement` lifecycles and methods on the server side
26
+ - `constructor`
27
+ - `connectedCallback`
28
+ - `attachShadow`
29
+ - `innerHTML`
30
+ - `[get|set|has]Attribute`
31
+ 1. Recursive rendering of nested custom elements
32
+ 1. Optional Declarative Shadow DOM (for producing purely content driven static pages)
33
+ 1. Metadata and runtime hints to support progressive hydration and lazy loading strategies
34
+
35
+ ## Installation
36
+
37
+ **wcc** can be installed from npm.
38
+
39
+ ```shell
40
+ $ npm install wc-compiler --save-dev
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ WCC exposes a few utilities to render your Web Components. Below is one example, with [full docs and more examples](https://wcc.greenwoodjs.io) available on the website.
46
+
47
+ 1. Given a custom element like so:
48
+ ```js
49
+ const template = document.createElement('template');
50
+
51
+ template.innerHTML = `
52
+ <style>
53
+ .footer {
54
+ color: white;
55
+ background-color: #192a27;
56
+ }
57
+ </style>
58
+
59
+ <footer class="footer">
60
+ <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
61
+ </footer>
62
+ `;
63
+
64
+ class Footer extends HTMLElement {
65
+ connectedCallback() {
66
+ if (!this.shadowRoot) {
67
+ this.attachShadow({ mode: 'open' });
68
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
69
+ }
70
+ }
71
+ }
72
+
73
+ export default Footer;
74
+
75
+ customElements.define('wcc-footer', Footer);
76
+ ```
77
+
78
+ 1. Using NodeJS, create a file that imports `renderToString` and provide it the path to your web component
79
+ ```js
80
+ import { renderToString } from 'wc-compiler';
81
+
82
+ const { html } = renderToString(new URL('./path/to/footer.js', import.meta.url));
83
+
84
+ console.debug({ html })
85
+ ```
86
+
87
+ 1. You will get the following html output that can be used in conjunction with your preferred site framework or templating solution.
88
+ ```html
89
+ <wcc-footer>
90
+ <template shadowroot="open">
91
+ <style>
92
+ .footer {
93
+ color: white;
94
+ background-color: #192a27;
95
+ }
96
+ </style>
97
+
98
+ <footer class="footer">
99
+ <h4>My Blog &copy; 2022</h4>
100
+ </footer>
101
+ </template>
102
+ </wcc-footer>
103
+ ```
104
+
105
+
106
+ > _**Make sure to test in Chrome, or other Declarative Shadow DOM compatible browser, otherwise you will need to include the [DSD polyfill](https://web.dev/declarative-shadow-dom/#polyfill).**_
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "wc-compiler",
3
+ "version": "0.1.0",
4
+ "description": "Experimental native Web Components compiler.",
5
+ "main": "src/wcc.js",
6
+ "type": "module",
7
+ "author": "Owen Buckley <owen@thegreenhouse.io>",
8
+ "license": "MIT",
9
+ "engines": {
10
+ "node": ">=14"
11
+ },
12
+ "files": [
13
+ "src/"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "lint": "eslint \"*.*js\" \"./src/**/**/*.js\" \"./test/**/**/*.js\"",
20
+ "develop": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html ./build.js\" \"http-server ./dist --open\"",
21
+ "build": "node ./build.js",
22
+ "serve": "node ./build.js && http-server ./dist --open",
23
+ "example:ssg": "node ./examples/ssg.js",
24
+ "example:ssr": "node ./examples/ssr.js",
25
+ "start": "npm run develop",
26
+ "test": "c8 mocha --parallel",
27
+ "test:tdd": "npm run test --watch"
28
+ },
29
+ "dependencies": {
30
+ "acorn": "^8.7.0",
31
+ "acorn-walk": "^8.2.0",
32
+ "parse5": "^6.0.1"
33
+ },
34
+ "devDependencies": {
35
+ "@mapbox/rehype-prism": "^0.8.0",
36
+ "c8": "^7.11.2",
37
+ "chai": "^4.3.6",
38
+ "concurrently": "^7.1.0",
39
+ "eslint": "^8.14.0",
40
+ "http-server": "^14.1.0",
41
+ "jsdom": "^19.0.0",
42
+ "mocha": "^9.2.2",
43
+ "nodemon": "^2.0.15",
44
+ "prismjs": "^1.28.0",
45
+ "simple.css": "^0.1.3",
46
+ "rehype-raw": "^6.1.1",
47
+ "rehype-stringify": "^9.0.3",
48
+ "remark-parse": "^10.0.1",
49
+ "remark-rehype": "^10.1.0",
50
+ "unified": "^10.1.2"
51
+ }
52
+ }
@@ -0,0 +1,141 @@
1
+ // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
2
+ class EventTarget { }
3
+
4
+ // https://developer.mozilla.org/en-US/docs/Web/API/Node
5
+ // EventTarget <- Node
6
+ class Node extends EventTarget {
7
+ constructor() {
8
+ super();
9
+ // console.debug('Node constructor');
10
+ }
11
+
12
+ // eslint-disable-next-line
13
+ cloneNode(deep) {
14
+ return this;
15
+ }
16
+
17
+ appendChild(node) {
18
+ this.innerHTML = this.innerHTML ? this.innerHTML += node.textContent : node.textContent;
19
+ }
20
+ }
21
+
22
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element
23
+ // EventTarget <- Node <- Element
24
+ class Element extends Node {
25
+ constructor() {
26
+ super();
27
+ // console.debug('Element constructor');
28
+ this.shadowRoot = null;
29
+ this.innerHTML;
30
+ this.attributes = {};
31
+ }
32
+
33
+ setAttribute(name, value) {
34
+ this.attributes[name] = value;
35
+ }
36
+
37
+ getAttribute(name) {
38
+ return this.attributes[name];
39
+ }
40
+
41
+ hasAttribute(name) {
42
+ return !!this.attributes[name];
43
+ }
44
+ }
45
+
46
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
47
+ // EventTarget <- Node <- Element <- HTMLElement
48
+ class HTMLElement extends Element {
49
+ constructor() {
50
+ super();
51
+ // console.debug('HTMLElement::constructor');
52
+ }
53
+
54
+ attachShadow(options) {
55
+ // console.debug('HTMLElement::attachShadow');
56
+ this.shadowRoot = new ShadowRoot(options);
57
+
58
+ return this.shadowRoot;
59
+ }
60
+
61
+ connectedCallback() {
62
+ // console.debug('HTMLElement::connectedCallback');
63
+ }
64
+
65
+ // https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#serialization
66
+ getInnerHTML(options = {}) {
67
+ return options.includeShadowRoots
68
+ ? `
69
+ <template shadowroot="${this.shadowRoot.mode}">
70
+ ${this.shadowRoot.innerHTML}
71
+ </template>
72
+ `
73
+ : this.shadowRoot.innerHTML;
74
+ }
75
+ }
76
+
77
+ // https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
78
+ // EventTarget <- Node <- DocumentFragment
79
+ class DocumentFragment extends Node {
80
+ // eslint-disable-next-line
81
+ constructor(contents) {
82
+ super();
83
+ // console.debug('DocumentFragment constructor', contents);
84
+ }
85
+ }
86
+
87
+ // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
88
+ // EventTarget <- Node <- DocumentFragment <- ShadowRoot
89
+ class ShadowRoot extends DocumentFragment {
90
+ constructor(options) {
91
+ super();
92
+ // console.debug('ShadowRoot constructor');
93
+ this.mode = options.mode || 'closed';
94
+ }
95
+ }
96
+
97
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement
98
+ // EventTarget <- Node <- Element <- HTMLElement <- HTMLTemplateElement
99
+ class HTMLTemplateElement extends HTMLElement {
100
+ constructor() {
101
+ super();
102
+ // console.debug('HTMLTemplateElement constructor');
103
+
104
+ this.content = new DocumentFragment(this.innerHTML);
105
+ }
106
+
107
+ set innerHTML(html) {
108
+ this.content.textContent = html;
109
+ }
110
+ }
111
+
112
+ const customElementsRegistry = {};
113
+
114
+ globalThis.document = {
115
+ createElement(tagName) {
116
+ switch (tagName) {
117
+
118
+ case 'template':
119
+ return new HTMLTemplateElement();
120
+
121
+ default:
122
+ return new HTMLElement();
123
+
124
+ }
125
+ },
126
+ createDocumentFragment(html) {
127
+ return new DocumentFragment(html);
128
+ }
129
+ };
130
+
131
+ globalThis.HTMLElement = HTMLElement;
132
+ globalThis.customElements = {
133
+ define: (tagName, BaseClass) => {
134
+ // console.debug('customElements.define => ', tagName);
135
+ customElementsRegistry[tagName] = BaseClass;
136
+ },
137
+ get: (tagName) => {
138
+ // console.debug('customElements.get => ', tagName);
139
+ return customElementsRegistry[tagName];
140
+ }
141
+ };
package/src/wcc.js ADDED
@@ -0,0 +1,130 @@
1
+ // this must come first
2
+ import './dom-shim.js';
3
+
4
+ import * as acorn from 'acorn';
5
+ import * as walk from 'acorn-walk';
6
+ import { parse, parseFragment, serialize } from 'parse5';
7
+
8
+ import fs from 'node:fs/promises';
9
+
10
+ let definitions;
11
+
12
+ async function renderComponentRoots(tree, includeShadowRoots = true) {
13
+ for (const node of tree.childNodes) {
14
+ if (node.tagName && node.tagName.indexOf('-') > 0) {
15
+ const { tagName } = node;
16
+ const { moduleURL } = definitions[tagName];
17
+ const elementInstance = await initializeCustomElement(moduleURL, tagName, node.attrs);
18
+
19
+ const shadowRootHtml = elementInstance.getInnerHTML({ includeShadowRoots });
20
+ const shadowRootTree = parseFragment(shadowRootHtml);
21
+
22
+ node.childNodes = node.childNodes.length === 0 ? shadowRootTree.childNodes : [...shadowRootTree.childNodes, ...node.childNodes];
23
+ }
24
+
25
+ if (node.childNodes && node.childNodes.length > 0) {
26
+ await renderComponentRoots(node, includeShadowRoots);
27
+ }
28
+
29
+ // does this only apply to `<template>` tags?
30
+ if (node.content && node.content.childNodes && node.content.childNodes.length > 0) {
31
+ await renderComponentRoots(node.content, includeShadowRoots);
32
+ }
33
+ }
34
+
35
+ return tree;
36
+ }
37
+
38
+ async function registerDependencies(moduleURL) {
39
+ const moduleContents = await fs.readFile(moduleURL, 'utf-8');
40
+
41
+ walk.simple(acorn.parse(moduleContents, {
42
+ ecmaVersion: 'latest',
43
+ sourceType: 'module'
44
+ }), {
45
+ async ImportDeclaration(node) {
46
+ const dependencyModuleURL = new URL(node.source.value, moduleURL);
47
+
48
+ await registerDependencies(dependencyModuleURL);
49
+ },
50
+ async ExpressionStatement(node) {
51
+ const { expression } = node;
52
+
53
+ if (expression.type === 'CallExpression' && expression.callee && expression.callee.object
54
+ && expression.callee.property && expression.callee.object.name === 'customElements'
55
+ && expression.callee.property.name === 'define') {
56
+
57
+ const tagName = node.expression.arguments[0].value;
58
+
59
+ definitions[tagName] = {
60
+ instanceName: node.expression.arguments[1].name,
61
+ moduleURL
62
+ };
63
+ }
64
+ }
65
+ });
66
+ }
67
+
68
+ async function initializeCustomElement(elementURL, tagName, attrs = []) {
69
+ await registerDependencies(elementURL);
70
+
71
+ const element = tagName
72
+ ? customElements.get(tagName)
73
+ : (await import(elementURL)).default;
74
+ const dataLoader = (await import(elementURL)).getData;
75
+ const data = dataLoader ? await dataLoader() : {};
76
+ const elementInstance = new element(data); // eslint-disable-line new-cap
77
+
78
+ attrs.forEach((attr) => {
79
+ elementInstance.setAttribute(attr.name, attr.value);
80
+
81
+ if (attr.name === 'hydrate') {
82
+ definitions[tagName].hydrate = attr.value;
83
+ }
84
+ });
85
+
86
+ await elementInstance.connectedCallback();
87
+
88
+ return elementInstance;
89
+ }
90
+
91
+ async function renderToString(elementURL, options = {}) {
92
+ definitions = [];
93
+
94
+ const { lightMode = false } = options;
95
+ const includeShadowRoots = !lightMode;
96
+
97
+ const elementInstance = await initializeCustomElement(elementURL);
98
+ const elementHtml = elementInstance.getInnerHTML({ includeShadowRoots });
99
+ const elementTree = parseFragment(elementHtml);
100
+ const finalTree = await renderComponentRoots(elementTree, includeShadowRoots);
101
+
102
+ return {
103
+ html: serialize(finalTree),
104
+ metadata: definitions
105
+ };
106
+ }
107
+
108
+ async function renderFromHTML(html, elements = [], options = {}) {
109
+ definitions = [];
110
+
111
+ const { lightMode = false } = options;
112
+ const includeShadowRoots = !lightMode;
113
+
114
+ for (const url of elements) {
115
+ await initializeCustomElement(url);
116
+ }
117
+
118
+ const elementTree = parse(html);
119
+ const finalTree = await renderComponentRoots(elementTree, includeShadowRoots);
120
+
121
+ return {
122
+ html: serialize(finalTree),
123
+ metadata: definitions
124
+ };
125
+ }
126
+
127
+ export {
128
+ renderToString,
129
+ renderFromHTML
130
+ };