wc-compiler 0.15.0 → 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.0",
3
+ "version": "0.16.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,10 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@projectevergreen/acorn-jsx-esm": "~0.1.0",
46
- "@projectevergreen/escodegen-esm": "~0.1.0",
47
- "acorn": "^8.7.0",
48
- "acorn-import-attributes": "^1.9.5",
49
- "acorn-walk": "^8.2.0",
50
- "parse5": "^6.0.1",
46
+ "acorn": "^8.14.0",
47
+ "acorn-walk": "^8.3.4",
48
+ "astring": "^1.9.0",
49
+ "parse5": "^7.2.1",
51
50
  "sucrase": "^3.35.0"
52
51
  },
53
52
  "devDependencies": {
@@ -57,7 +56,7 @@
57
56
  "@babel/preset-react": "^7.24.1",
58
57
  "@ls-lint/ls-lint": "^1.10.0",
59
58
  "@mapbox/rehype-prism": "^0.8.0",
60
- "@rollup/plugin-commonjs": "^25.0.7",
59
+ "@rollup/plugin-commonjs": "^28.0.0",
61
60
  "@rollup/plugin-node-resolve": "^15.2.3",
62
61
  "c8": "^7.11.2",
63
62
  "chai": "^4.3.6",
@@ -79,7 +78,7 @@
79
78
  "remark-rehype": "^10.1.0",
80
79
  "remark-toc": "^8.0.1",
81
80
  "rimraf": "^3.0.2",
82
- "rollup": "^3.29.3",
81
+ "rollup": "^4.26.0",
83
82
  "simple.css": "^0.1.3",
84
83
  "unified": "^10.1.2"
85
84
  }
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/jsx-loader.js CHANGED
@@ -2,12 +2,12 @@
2
2
  // https://nodejs.org/api/esm.html#esm_loaders
3
3
  import * as acorn from 'acorn';
4
4
  import * as walk from 'acorn-walk';
5
- import { generate } from '@projectevergreen/escodegen-esm';
5
+ import { generate } from 'astring';
6
6
  import fs from 'fs';
7
+ // ideally we can eventually adopt an ESM compatible version of this plugin
8
+ // https://github.com/acornjs/acorn-jsx/issues/112
7
9
  import jsx from '@projectevergreen/acorn-jsx-esm';
8
10
  import { parse, parseFragment, serialize } from 'parse5';
9
- // Need an acorn plugin for now - https://github.com/ProjectEvergreen/greenwood/issues/1218
10
- import { importAttributes } from 'acorn-import-attributes';
11
11
  import { transform } from 'sucrase';
12
12
 
13
13
  const jsxRegex = /\.(jsx)$/;
@@ -32,7 +32,7 @@ export function getParser(moduleURL) {
32
32
  }
33
33
 
34
34
  return {
35
- parser: acorn.Parser.extend(jsx(), importAttributes),
35
+ parser: acorn.Parser.extend(jsx()),
36
36
  config: {
37
37
  // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
38
38
  ...walk.base,
@@ -243,7 +243,7 @@ export function parseJsx(moduleURL) {
243
243
  const hasOwnObservedAttributes = undefined;
244
244
  let inferredObservability = false;
245
245
  let observedAttributes = [];
246
- let tree = acorn.Parser.extend(jsx(), importAttributes).parse(result.code, {
246
+ let tree = acorn.Parser.extend(jsx()).parse(result.code, {
247
247
  ecmaVersion: 'latest',
248
248
  sourceType: 'module'
249
249
  });
package/src/wcc.js CHANGED
@@ -1,41 +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
- import { generate } from '@projectevergreen/escodegen-esm';
7
+ import { generate } from 'astring';
8
8
  import { getParser, parseJsx } from './jsx-loader.js';
9
- import { parse, parseFragment, serialize } from 'parse5';
10
- // Need an acorn plugin for now - https://github.com/ProjectEvergreen/greenwood/issues/1218
11
- import { importAttributes } from 'acorn-import-attributes';
9
+ import { serialize } from 'parse5';
12
10
  import { transform } from 'sucrase';
13
11
  import fs from 'fs';
14
12
 
15
- // https://developer.mozilla.org/en-US/docs/Glossary/Void_element
16
- const VOID_ELEMENTS = [
17
- 'area',
18
- 'base',
19
- 'br',
20
- 'col',
21
- 'embed',
22
- 'hr',
23
- 'img',
24
- 'input',
25
- 'link',
26
- 'meta',
27
- 'param', // deprecated
28
- 'source',
29
- 'track',
30
- 'wbr'
31
- ];
32
-
33
- function getParse(html) {
34
- return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
35
- ? parse
36
- : parseFragment;
37
- }
38
-
39
13
  function isCustomElementDefinitionNode(node) {
40
14
  const { expression } = node;
41
15
 
@@ -47,7 +21,7 @@ function isCustomElementDefinitionNode(node) {
47
21
  async function renderComponentRoots(tree, definitions) {
48
22
  for (const node of tree.childNodes) {
49
23
  if (node.tagName && node.tagName.indexOf('-') > 0) {
50
- const { tagName } = node;
24
+ const { attrs, tagName } = node;
51
25
 
52
26
  if (definitions[tagName]) {
53
27
  const { moduleURL } = definitions[tagName];
@@ -55,31 +29,35 @@ async function renderComponentRoots(tree, definitions) {
55
29
 
56
30
  if (elementInstance) {
57
31
  const hasShadow = elementInstance.shadowRoot;
58
- const elementHtml = hasShadow
59
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
60
- : elementInstance.innerHTML;
61
- const elementTree = parseFragment(elementHtml);
62
- const hasLight = elementTree.childNodes > 0;
63
-
64
- node.childNodes = node.childNodes.length === 0 && hasLight && !hasShadow
65
- ? elementTree.childNodes
66
- : hasShadow
67
- ? [...elementTree.childNodes, ...node.childNodes]
68
- : elementTree.childNodes;
32
+
33
+ node.childNodes = hasShadow
34
+ ? [...elementInstance.shadowRoot.childNodes, ...node.childNodes]
35
+ : elementInstance.childNodes;
69
36
  } else {
70
37
  console.warn(`WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`);
71
38
  }
72
39
  } else {
73
40
  console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it.`);
74
41
  }
42
+
43
+ attrs.forEach((attr) => {
44
+ if (attr.name === 'hydrate') {
45
+ definitions[tagName].hydrate = attr.value;
46
+ }
47
+ });
48
+
75
49
  }
76
50
 
77
51
  if (node.childNodes && node.childNodes.length > 0) {
78
52
  await renderComponentRoots(node, definitions);
79
53
  }
80
54
 
55
+ if (node.shadowRoot && node.shadowRoot.childNodes?.length > 0) {
56
+ await renderComponentRoots(node.shadowRoot, definitions);
57
+ }
58
+
81
59
  // does this only apply to `<template>` tags?
82
- if (node.content && node.content.childNodes && node.content.childNodes.length > 0) {
60
+ if (node.content && node.content.childNodes?.length > 0) {
83
61
  await renderComponentRoots(node.content, definitions);
84
62
  }
85
63
  }
@@ -100,7 +78,7 @@ function registerDependencies(moduleURL, definitions, depth = 0) {
100
78
  ...walk.base
101
79
  };
102
80
 
103
- walk.simple(parser.extend(importAttributes).parse(result.code, {
81
+ walk.simple(parser.parse(result.code, {
104
82
  ecmaVersion: 'latest',
105
83
  sourceType: 'module'
106
84
  }), {
@@ -151,7 +129,7 @@ async function getTagName(moduleURL) {
151
129
  };
152
130
  let tagName;
153
131
 
154
- walk.simple(parser.extend(importAttributes).parse(result.code, {
132
+ walk.simple(parser.parse(result.code, {
155
133
  ecmaVersion: 'latest',
156
134
  sourceType: 'module'
157
135
  }), {
@@ -165,40 +143,7 @@ async function getTagName(moduleURL) {
165
143
  return tagName;
166
144
  }
167
145
 
168
- function renderLightDomChildren(childNodes, iHTML = '') {
169
- let innerHTML = iHTML;
170
-
171
- childNodes.forEach((child) => {
172
- const { nodeName, attrs = [], value } = child;
173
-
174
- if (nodeName !== '#text') {
175
- innerHTML += `<${nodeName}`;
176
-
177
- if (attrs.length > 0) {
178
- attrs.forEach(attr => {
179
- innerHTML += ` ${attr.name}="${attr.value}"`;
180
- });
181
- }
182
-
183
- innerHTML += '>';
184
-
185
- if (child.childNodes.length > 0) {
186
- innerHTML = renderLightDomChildren(child.childNodes, innerHTML);
187
- }
188
-
189
- innerHTML += VOID_ELEMENTS.includes(nodeName)
190
- ? ''
191
- : `</${nodeName}>`;
192
- } else if (nodeName === '#text') {
193
- innerHTML += value;
194
- }
195
- });
196
-
197
- return innerHTML;
198
- }
199
-
200
146
  async function initializeCustomElement(elementURL, tagName, node = {}, definitions = [], isEntry, props = {}) {
201
- const { attrs = [], childNodes = [] } = node;
202
147
 
203
148
  if (!tagName) {
204
149
  const depth = isEntry ? 1 : 0;
@@ -210,28 +155,15 @@ async function initializeCustomElement(elementURL, tagName, node = {}, definitio
210
155
  const { href } = elementURL;
211
156
  const element = customElements.get(tagName) ?? (await import(href)).default;
212
157
  const dataLoader = (await import(href)).getData;
213
- const data = props
214
- ? props
215
- : dataLoader
216
- ? await dataLoader(props)
217
- : {};
158
+ const data = props ? props : dataLoader ? await dataLoader(props) : {};
218
159
 
219
160
  if (element) {
220
161
  const elementInstance = new element(data); // eslint-disable-line new-cap
221
162
 
222
- // support for HTML (Light DOM) Web Components
223
- elementInstance.innerHTML = renderLightDomChildren(childNodes);
163
+ Object.assign(elementInstance, node);
224
164
 
225
- attrs.forEach((attr) => {
226
- elementInstance.setAttribute(attr.name, attr.value);
227
-
228
- if (attr.name === 'hydrate') {
229
- definitions[tagName].hydrate = attr.value;
230
- }
231
- });
232
-
233
165
  await elementInstance.connectedCallback();
234
-
166
+
235
167
  return elementInstance;
236
168
  }
237
169
  }
@@ -241,22 +173,31 @@ async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
241
173
  const elementTagName = wrappingEntryTag && await getTagName(elementURL);
242
174
  const isEntry = !!elementTagName;
243
175
  const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry, props);
176
+
244
177
  let html;
245
178
 
246
179
  // in case the entry point isn't valid
247
180
  if (elementInstance) {
248
- const elementHtml = elementInstance.shadowRoot
249
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
250
- : elementInstance.innerHTML;
251
- const elementTree = getParse(elementHtml)(elementHtml);
252
- 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
+ );
253
194
 
254
195
  html = wrappingEntryTag && elementTagName ? `
255
196
  <${elementTagName}>
256
- ${serialize(finalTree)}
197
+ ${serialize(elementInstance)}
257
198
  </${elementTagName}>
258
199
  `
259
- : serialize(finalTree);
200
+ : serialize(elementInstance);
260
201
  } else {
261
202
  console.warn('WARNING: No custom element class found for this entry point.');
262
203
  }