wc-compiler 0.15.1 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wc-compiler",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "acorn": "^8.14.0",
47
47
  "acorn-walk": "^8.3.4",
48
48
  "astring": "^1.9.0",
49
- "parse5": "^6.0.1",
49
+ "parse5": "^7.2.1",
50
50
  "sucrase": "^3.35.0"
51
51
  },
52
52
  "devDependencies": {
package/src/dom-shim.js CHANGED
@@ -1,3 +1,57 @@
1
+ /* eslint-disable no-warning-comments */
2
+
3
+ import { parse, parseFragment, serialize } from 'parse5';
4
+
5
+ export function getParse(html) {
6
+ return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
7
+ ? parse
8
+ : parseFragment;
9
+ }
10
+
11
+ function isShadowRoot(element) {
12
+ return Object.getPrototypeOf(element).constructor.name === 'ShadowRoot';
13
+ }
14
+
15
+ function deepClone(obj, map = new WeakMap()) {
16
+ if (obj === null || typeof obj !== 'object') {
17
+ return obj;
18
+ }
19
+
20
+ if (typeof obj === 'function') {
21
+ const clonedFn = obj.bind({});
22
+ Object.assign(clonedFn, obj);
23
+ return clonedFn;
24
+ }
25
+
26
+ if (map.has(obj)) {
27
+ return map.get(obj);
28
+ }
29
+
30
+ const result = Array.isArray(obj) ? [] : {};
31
+ map.set(obj, result);
32
+
33
+ for (const key of Object.keys(obj)) {
34
+ result[key] = deepClone(obj[key], map);
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ // Creates an empty parse5 element without the parse5 overhead resulting in better performance
41
+ function getParse5ElementDefaults(element, tagName) {
42
+ return {
43
+ addEventListener: noop,
44
+ attrs: [],
45
+ parentNode: element.parentNode,
46
+ childNodes: [],
47
+ nodeName: tagName,
48
+ tagName: tagName,
49
+ namespaceURI: 'http://www.w3.org/1999/xhtml',
50
+ // eslint-disable-next-line no-extra-parens
51
+ ...(tagName === 'template' ? { content: { nodeName: '#document-fragment', childNodes: [] } } : {})
52
+ };
53
+ }
54
+
1
55
  function noop() { }
2
56
 
3
57
  // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
@@ -19,13 +73,81 @@ class EventTarget {
19
73
  // EventTarget <- Node
20
74
  // TODO should be an interface?
21
75
  class Node extends EventTarget {
22
- // eslint-disable-next-line
76
+ constructor() {
77
+ super();
78
+ // Parse5 properties
79
+ this.attrs = [];
80
+ this.parentNode = null;
81
+ this.childNodes = [];
82
+ this.nodeName = '';
83
+ }
84
+
23
85
  cloneNode(deep) {
24
- return this;
86
+ return deep ? deepClone(this) : Object.assign({}, this);
25
87
  }
26
88
 
27
89
  appendChild(node) {
28
- this.innerHTML = this.innerHTML ? this.innerHTML += node.innerHTML : node.innerHTML;
90
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
91
+
92
+ if (node.parentNode) {
93
+ node.parentNode?.removeChild?.(node);
94
+ }
95
+
96
+ if (node.nodeName === 'template') {
97
+ if (isShadowRoot(this) && this.mode) {
98
+ node.attrs = [{ name: 'shadowrootmode', value: this.mode }];
99
+ childNodes.push(node);
100
+ node.parentNode = this;
101
+ } else {
102
+ this.childNodes = [...this.childNodes, ...node.content.childNodes];
103
+ }
104
+ } else if (node instanceof DocumentFragment) {
105
+ this.childNodes = [...this.childNodes, ...node.childNodes];
106
+ } else {
107
+ childNodes.push(node);
108
+ node.parentNode = this;
109
+ }
110
+
111
+ return node;
112
+ }
113
+
114
+ removeChild(node) {
115
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
116
+ if (!childNodes || !childNodes.length) {
117
+ return null;
118
+ }
119
+
120
+ const index = childNodes.indexOf(node);
121
+ if (index === -1) {
122
+ return null;
123
+ }
124
+
125
+ childNodes.splice(index, 1);
126
+ node.parentNode = null;
127
+
128
+ return node;
129
+ }
130
+
131
+ get textContent() {
132
+ if (this.nodeName === '#text') {
133
+ return this.value || '';
134
+ }
135
+
136
+ return this.childNodes
137
+ .map((child) => child.nodeName === '#text' ? child.value : child.textContent)
138
+ .join('');
139
+ }
140
+
141
+ set textContent(value) {
142
+ this.childNodes = [];
143
+
144
+ if (value) {
145
+ const textNode = new Node();
146
+ textNode.nodeName = '#text';
147
+ textNode.value = value;
148
+ textNode.parentNode = this;
149
+ this.childNodes.push(textNode);
150
+ }
29
151
  }
30
152
  }
31
153
 
@@ -34,33 +156,44 @@ class Node extends EventTarget {
34
156
  class Element extends Node {
35
157
  constructor() {
36
158
  super();
37
- this.shadowRoot = null;
38
- this.innerHTML = '';
39
- this.attributes = {};
40
159
  }
41
160
 
42
161
  attachShadow(options) {
43
162
  this.shadowRoot = new ShadowRoot(options);
44
-
163
+ this.shadowRoot.parentNode = this;
45
164
  return this.shadowRoot;
46
165
  }
47
166
 
48
- // https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#serialization
49
- // eslint-disable-next-line
50
- getInnerHTML() {
51
- return this.shadowRoot ? this.shadowRoot.innerHTML : this.innerHTML;
167
+ getHTML({ serializableShadowRoots = false }) {
168
+ return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable ? this.shadowRoot.innerHTML : '';
52
169
  }
53
170
 
54
- setAttribute(name, value) {
55
- this.attributes[name] = value;
171
+ get innerHTML() {
172
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
173
+ return childNodes ? serialize({ childNodes }) : '';
56
174
  }
57
175
 
58
- getAttribute(name) {
59
- return this.attributes[name];
176
+ set innerHTML(html) {
177
+ (this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes;
60
178
  }
61
179
 
62
180
  hasAttribute(name) {
63
- return !!this.attributes[name];
181
+ return this.attrs.some((attr) => attr.name === name);
182
+ }
183
+
184
+ getAttribute(name) {
185
+ const attr = this.attrs.find((attr) => attr.name === name);
186
+ return attr ? attr.value : null;
187
+ }
188
+
189
+ setAttribute(name, value) {
190
+ const attr = this.attrs?.find((attr) => attr.name === name);
191
+
192
+ if (attr) {
193
+ attr.value = value;
194
+ } else {
195
+ this.attrs?.push({ name, value });
196
+ }
64
197
  }
65
198
  }
66
199
 
@@ -75,7 +208,7 @@ class Document extends Node {
75
208
  return new HTMLTemplateElement();
76
209
 
77
210
  default:
78
- return new HTMLElement();
211
+ return new HTMLElement(tagName);
79
212
 
80
213
  }
81
214
  }
@@ -88,6 +221,10 @@ class Document extends Node {
88
221
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
89
222
  // EventTarget <- Node <- Element <- HTMLElement
90
223
  class HTMLElement extends Element {
224
+ constructor(tagName) {
225
+ super();
226
+ Object.assign(this, getParse5ElementDefaults(this, tagName));
227
+ }
91
228
  connectedCallback() { }
92
229
  }
93
230
 
@@ -100,9 +237,18 @@ class DocumentFragment extends Node { }
100
237
  class ShadowRoot extends DocumentFragment {
101
238
  constructor(options) {
102
239
  super();
103
- this.mode = options.mode || 'closed';
240
+ this.mode = options.mode ?? 'closed';
241
+ this.serializable = options.serializable ?? false;
104
242
  this.adoptedStyleSheets = [];
105
243
  }
244
+
245
+ get innerHTML() {
246
+ return this.childNodes?.[0]?.content?.childNodes ? serialize({ childNodes: this.childNodes[0].content.childNodes }) : '';
247
+ }
248
+
249
+ set innerHTML(html) {
250
+ this.childNodes = getParse(html)(`<template shadowrootmode="${this.mode}">${html}</template>`).childNodes;
251
+ }
106
252
  }
107
253
 
108
254
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement
@@ -110,22 +256,11 @@ class ShadowRoot extends DocumentFragment {
110
256
  class HTMLTemplateElement extends HTMLElement {
111
257
  constructor() {
112
258
  super();
113
- this.content = new DocumentFragment();
114
- }
115
-
116
- // TODO open vs closed shadow root
117
- set innerHTML(html) {
118
- if (this.content) {
119
- this.content.innerHTML = `
120
- <template shadowrootmode="open">
121
- ${html}
122
- </template>
123
- `;
124
- }
125
- }
126
-
127
- get innerHTML() {
128
- return this.content && this.content.innerHTML ? this.content.innerHTML : undefined;
259
+ // Gets element defaults for template element instead of parsing a
260
+ // <template></template> with parse5. Results in better performance
261
+ // when creating templates
262
+ Object.assign(this, getParse5ElementDefaults(this, 'template'));
263
+ this.content.cloneNode = this.cloneNode.bind(this);
129
264
  }
130
265
  }
131
266
 
@@ -156,4 +291,5 @@ globalThis.addEventListener = globalThis.addEventListener ?? noop;
156
291
  globalThis.document = globalThis.document ?? new Document();
157
292
  globalThis.customElements = globalThis.customElements ?? new CustomElementsRegistry();
158
293
  globalThis.HTMLElement = globalThis.HTMLElement ?? HTMLElement;
294
+ globalThis.DocumentFragment = globalThis.DocumentFragment ?? DocumentFragment;
159
295
  globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? CSSStyleSheet;
package/src/wcc.js CHANGED
@@ -1,39 +1,15 @@
1
1
  /* eslint-disable max-depth */
2
2
  // this must come first
3
- import './dom-shim.js';
3
+ import { getParse } from './dom-shim.js';
4
4
 
5
5
  import * as acorn from 'acorn';
6
6
  import * as walk from 'acorn-walk';
7
7
  import { generate } from 'astring';
8
8
  import { getParser, parseJsx } from './jsx-loader.js';
9
- import { parse, parseFragment, serialize } from 'parse5';
9
+ import { serialize } from 'parse5';
10
10
  import { transform } from 'sucrase';
11
11
  import fs from 'fs';
12
12
 
13
- // https://developer.mozilla.org/en-US/docs/Glossary/Void_element
14
- const VOID_ELEMENTS = [
15
- 'area',
16
- 'base',
17
- 'br',
18
- 'col',
19
- 'embed',
20
- 'hr',
21
- 'img',
22
- 'input',
23
- 'link',
24
- 'meta',
25
- 'param', // deprecated
26
- 'source',
27
- 'track',
28
- 'wbr'
29
- ];
30
-
31
- function getParse(html) {
32
- return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
33
- ? parse
34
- : parseFragment;
35
- }
36
-
37
13
  function isCustomElementDefinitionNode(node) {
38
14
  const { expression } = node;
39
15
 
@@ -45,7 +21,7 @@ function isCustomElementDefinitionNode(node) {
45
21
  async function renderComponentRoots(tree, definitions) {
46
22
  for (const node of tree.childNodes) {
47
23
  if (node.tagName && node.tagName.indexOf('-') > 0) {
48
- const { tagName } = node;
24
+ const { attrs, tagName } = node;
49
25
 
50
26
  if (definitions[tagName]) {
51
27
  const { moduleURL } = definitions[tagName];
@@ -53,31 +29,35 @@ async function renderComponentRoots(tree, definitions) {
53
29
 
54
30
  if (elementInstance) {
55
31
  const hasShadow = elementInstance.shadowRoot;
56
- const elementHtml = hasShadow
57
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
58
- : elementInstance.innerHTML;
59
- const elementTree = parseFragment(elementHtml);
60
- const hasLight = elementTree.childNodes > 0;
61
-
62
- node.childNodes = node.childNodes.length === 0 && hasLight && !hasShadow
63
- ? elementTree.childNodes
64
- : hasShadow
65
- ? [...elementTree.childNodes, ...node.childNodes]
66
- : elementTree.childNodes;
32
+
33
+ node.childNodes = hasShadow
34
+ ? [...elementInstance.shadowRoot.childNodes, ...node.childNodes]
35
+ : elementInstance.childNodes;
67
36
  } else {
68
37
  console.warn(`WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`);
69
38
  }
70
39
  } else {
71
40
  console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it.`);
72
41
  }
42
+
43
+ attrs.forEach((attr) => {
44
+ if (attr.name === 'hydrate') {
45
+ definitions[tagName].hydrate = attr.value;
46
+ }
47
+ });
48
+
73
49
  }
74
50
 
75
51
  if (node.childNodes && node.childNodes.length > 0) {
76
52
  await renderComponentRoots(node, definitions);
77
53
  }
78
54
 
55
+ if (node.shadowRoot && node.shadowRoot.childNodes?.length > 0) {
56
+ await renderComponentRoots(node.shadowRoot, definitions);
57
+ }
58
+
79
59
  // does this only apply to `<template>` tags?
80
- if (node.content && node.content.childNodes && node.content.childNodes.length > 0) {
60
+ if (node.content && node.content.childNodes?.length > 0) {
81
61
  await renderComponentRoots(node.content, definitions);
82
62
  }
83
63
  }
@@ -163,40 +143,7 @@ async function getTagName(moduleURL) {
163
143
  return tagName;
164
144
  }
165
145
 
166
- function renderLightDomChildren(childNodes, iHTML = '') {
167
- let innerHTML = iHTML;
168
-
169
- childNodes.forEach((child) => {
170
- const { nodeName, attrs = [], value } = child;
171
-
172
- if (nodeName !== '#text') {
173
- innerHTML += `<${nodeName}`;
174
-
175
- if (attrs.length > 0) {
176
- attrs.forEach(attr => {
177
- innerHTML += ` ${attr.name}="${attr.value}"`;
178
- });
179
- }
180
-
181
- innerHTML += '>';
182
-
183
- if (child.childNodes.length > 0) {
184
- innerHTML = renderLightDomChildren(child.childNodes, innerHTML);
185
- }
186
-
187
- innerHTML += VOID_ELEMENTS.includes(nodeName)
188
- ? ''
189
- : `</${nodeName}>`;
190
- } else if (nodeName === '#text') {
191
- innerHTML += value;
192
- }
193
- });
194
-
195
- return innerHTML;
196
- }
197
-
198
146
  async function initializeCustomElement(elementURL, tagName, node = {}, definitions = [], isEntry, props = {}) {
199
- const { attrs = [], childNodes = [] } = node;
200
147
 
201
148
  if (!tagName) {
202
149
  const depth = isEntry ? 1 : 0;
@@ -208,28 +155,15 @@ async function initializeCustomElement(elementURL, tagName, node = {}, definitio
208
155
  const { href } = elementURL;
209
156
  const element = customElements.get(tagName) ?? (await import(href)).default;
210
157
  const dataLoader = (await import(href)).getData;
211
- const data = props
212
- ? props
213
- : dataLoader
214
- ? await dataLoader(props)
215
- : {};
158
+ const data = props ? props : dataLoader ? await dataLoader(props) : {};
216
159
 
217
160
  if (element) {
218
161
  const elementInstance = new element(data); // eslint-disable-line new-cap
219
162
 
220
- // support for HTML (Light DOM) Web Components
221
- elementInstance.innerHTML = renderLightDomChildren(childNodes);
163
+ Object.assign(elementInstance, node);
222
164
 
223
- attrs.forEach((attr) => {
224
- elementInstance.setAttribute(attr.name, attr.value);
225
-
226
- if (attr.name === 'hydrate') {
227
- definitions[tagName].hydrate = attr.value;
228
- }
229
- });
230
-
231
165
  await elementInstance.connectedCallback();
232
-
166
+
233
167
  return elementInstance;
234
168
  }
235
169
  }
@@ -239,22 +173,31 @@ async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
239
173
  const elementTagName = wrappingEntryTag && await getTagName(elementURL);
240
174
  const isEntry = !!elementTagName;
241
175
  const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry, props);
176
+
242
177
  let html;
243
178
 
244
179
  // in case the entry point isn't valid
245
180
  if (elementInstance) {
246
- const elementHtml = elementInstance.shadowRoot
247
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
248
- : elementInstance.innerHTML;
249
- const elementTree = getParse(elementHtml)(elementHtml);
250
- const finalTree = await renderComponentRoots(elementTree, definitions);
181
+ elementInstance.nodeName = elementTagName ?? '';
182
+ elementInstance.tagName = elementTagName ?? '';
183
+
184
+ await renderComponentRoots(
185
+ elementInstance.shadowRoot
186
+ ?
187
+ {
188
+ nodeName: '#document-fragment',
189
+ childNodes: [elementInstance]
190
+ }
191
+ : elementInstance,
192
+ definitions
193
+ );
251
194
 
252
195
  html = wrappingEntryTag && elementTagName ? `
253
196
  <${elementTagName}>
254
- ${serialize(finalTree)}
197
+ ${serialize(elementInstance)}
255
198
  </${elementTagName}>
256
199
  `
257
- : serialize(finalTree);
200
+ : serialize(elementInstance);
258
201
  } else {
259
202
  console.warn('WARNING: No custom element class found for this entry point.');
260
203
  }