wc-compiler 0.4.0 → 0.6.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.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "main": "src/wcc.js",
6
6
  "type": "module",
@@ -11,33 +11,36 @@
11
11
  },
12
12
  "files": [
13
13
  "src/",
14
- "dist/wcc.dist.js"
14
+ "dist/wcc.dist.cjs"
15
15
  ],
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
19
  "scripts": {
20
20
  "clean": "rimraf ./dist",
21
- "lint": "eslint \"*.*js\" \"./src/**/**/*.js\" \"./test/**/**/*.js\"",
22
- "develop": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html ./build.js\" \"http-server ./dist --open\"",
21
+ "lint": "eslint \"*.*js\" \"./src/**/**/*.js*\" \"./test/**/**/*.js*\"",
22
+ "develop": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html,jsx ./build.js\" \"http-server ./dist --open\"",
23
23
  "build": "node ./build.js",
24
24
  "serve": "node ./build.js && http-server ./dist --open",
25
- "example:ssg": "node ./examples/ssg.js",
26
- "example:ssr": "node ./examples/ssr.js",
27
25
  "start": "npm run develop",
28
- "test": "c8 mocha --parallel",
29
- "test:tdd": "npm run test --watch",
26
+ "test": "mocha --exclude \"./test/cases/jsx/**\" \"./test/**/**/*.spec.js\"",
27
+ "test:jsx": "c8 node --experimental-loader ./test-jsx-loader.js ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
28
+ "test:tdd": "npm run test -- --watch",
29
+ "test:tdd:jsx": "npm run test:jsx -- --watch",
30
30
  "dist": "rollup -c rollup.config.js",
31
31
  "prepublishOnly": "npm run clean && npm run dist"
32
32
  },
33
33
  "dependencies": {
34
34
  "acorn": "^8.7.0",
35
+ "acorn-jsx": "^5.3.2",
35
36
  "acorn-walk": "^8.2.0",
37
+ "escodegen": "^2.0.0",
36
38
  "parse5": "^6.0.1"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@mapbox/rehype-prism": "^0.8.0",
40
42
  "@rollup/plugin-commonjs": "^22.0.0",
43
+ "@rollup/plugin-json": "^4.1.0",
41
44
  "@rollup/plugin-node-resolve": "^13.3.0",
42
45
  "c8": "^7.11.2",
43
46
  "chai": "^4.3.6",
@@ -46,6 +49,7 @@
46
49
  "http-server": "^14.1.0",
47
50
  "jsdom": "^19.0.0",
48
51
  "mocha": "^9.2.2",
52
+ "node-fetch": "^3.2.6",
49
53
  "nodemon": "^2.0.15",
50
54
  "prismjs": "^1.28.0",
51
55
  "rehype-autolink-headings": "^6.1.1",
package/src/dom-shim.js CHANGED
@@ -1,15 +1,16 @@
1
+ function noop() { }
2
+
1
3
  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
2
- class EventTarget { }
4
+ class EventTarget {
5
+ constructor() {
6
+ this.addEventListener = noop;
7
+ }
8
+ }
3
9
 
4
10
  // https://developer.mozilla.org/en-US/docs/Web/API/Node
5
11
  // EventTarget <- Node
6
12
  // TODO should be an interface?
7
13
  class Node extends EventTarget {
8
- constructor() {
9
- super();
10
- // console.debug('Node constructor');
11
- }
12
-
13
14
  // eslint-disable-next-line
14
15
  cloneNode(deep) {
15
16
  return this;
@@ -25,9 +26,8 @@ class Node extends EventTarget {
25
26
  class Element extends Node {
26
27
  constructor() {
27
28
  super();
28
- // console.debug('Element constructor');
29
29
  this.shadowRoot = null;
30
- this.innerHTML;
30
+ this.innerHTML = '';
31
31
  this.attributes = {};
32
32
  }
33
33
 
@@ -44,24 +44,37 @@ class Element extends Node {
44
44
  }
45
45
  }
46
46
 
47
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document
48
+ // EventTarget <- Node <- Document
49
+ class Document extends Node {
50
+
51
+ createElement(tagName) {
52
+ switch (tagName) {
53
+
54
+ case 'template':
55
+ return new HTMLTemplateElement();
56
+
57
+ default:
58
+ return new HTMLElement();
59
+
60
+ }
61
+ }
62
+
63
+ createDocumentFragment(html) {
64
+ return new DocumentFragment(html);
65
+ }
66
+ }
67
+
47
68
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
48
69
  // EventTarget <- Node <- Element <- HTMLElement
49
70
  class HTMLElement extends Element {
50
- constructor() {
51
- super();
52
- // console.debug('HTMLElement::constructor');
53
- }
54
-
55
71
  attachShadow(options) {
56
- // console.debug('HTMLElement::attachShadow');
57
72
  this.shadowRoot = new ShadowRoot(options);
58
73
 
59
74
  return this.shadowRoot;
60
75
  }
61
76
 
62
- connectedCallback() {
63
- // console.debug('HTMLElement::connectedCallback');
64
- }
77
+ connectedCallback() { }
65
78
 
66
79
  // https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#serialization
67
80
  // eslint-disable-next-line
@@ -72,20 +85,13 @@ class HTMLElement extends Element {
72
85
 
73
86
  // https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
74
87
  // EventTarget <- Node <- DocumentFragment
75
- class DocumentFragment extends Node {
76
- // eslint-disable-next-line
77
- constructor(contents) {
78
- super();
79
- // console.debug('DocumentFragment constructor', contents);
80
- }
81
- }
88
+ class DocumentFragment extends Node { }
82
89
 
83
90
  // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
84
91
  // EventTarget <- Node <- DocumentFragment <- ShadowRoot
85
92
  class ShadowRoot extends DocumentFragment {
86
93
  constructor(options) {
87
94
  super();
88
- // console.debug('ShadowRoot constructor');
89
95
  this.mode = options.mode || 'closed';
90
96
  }
91
97
  }
@@ -95,18 +101,18 @@ class ShadowRoot extends DocumentFragment {
95
101
  class HTMLTemplateElement extends HTMLElement {
96
102
  constructor() {
97
103
  super();
98
- // console.debug('HTMLTemplateElement constructor');
99
-
100
104
  this.content = new DocumentFragment();
101
105
  }
102
106
 
103
107
  // TODO open vs closed shadow root
104
108
  set innerHTML(html) {
105
- this.content.innerHTML = `
106
- <template shadowroot="open">
107
- ${html}
108
- </template>
109
- `;
109
+ if (this.content) {
110
+ this.content.innerHTML = `
111
+ <template shadowroot="open">
112
+ ${html}
113
+ </template>
114
+ `;
115
+ }
110
116
  }
111
117
 
112
118
  get innerHTML() {
@@ -114,33 +120,24 @@ class HTMLTemplateElement extends HTMLElement {
114
120
  }
115
121
  }
116
122
 
117
- const customElementsRegistry = {};
118
-
119
- globalThis.document = {
120
- createElement(tagName) {
121
- switch (tagName) {
122
-
123
- case 'template':
124
- return new HTMLTemplateElement();
125
-
126
- default:
127
- return new HTMLElement();
123
+ // https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry
124
+ class CustomElementsRegistry {
125
+ constructor() {
126
+ this.customElementsRegistry = {};
127
+ }
128
128
 
129
- }
130
- },
131
- createDocumentFragment(html) {
132
- return new DocumentFragment(html);
129
+ define(tagName, BaseClass) {
130
+ this.customElementsRegistry[tagName] = BaseClass;
133
131
  }
134
- };
135
132
 
136
- globalThis.HTMLElement = HTMLElement;
137
- globalThis.customElements = {
138
- define: (tagName, BaseClass) => {
139
- // console.debug('customElements.define => ', tagName);
140
- customElementsRegistry[tagName] = BaseClass;
141
- },
142
- get: (tagName) => {
143
- // console.debug('customElements.get => ', tagName);
144
- return customElementsRegistry[tagName];
133
+ get(tagName) {
134
+ return this.customElementsRegistry[tagName];
145
135
  }
146
- };
136
+ }
137
+
138
+ // mock top level aliases (globalThis === window)
139
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window
140
+ globalThis.addEventListener = noop;
141
+ globalThis.document = new Document();
142
+ globalThis.customElements = new CustomElementsRegistry();
143
+ globalThis.HTMLElement = HTMLElement;
@@ -0,0 +1,273 @@
1
+ /* eslint-disable max-depth, complexity */
2
+ // https://nodejs.org/api/esm.html#esm_loaders
3
+ import * as acorn from 'acorn';
4
+ import * as walk from 'acorn-walk';
5
+ import escodegen from 'escodegen';
6
+ import fs from 'fs';
7
+ import jsx from 'acorn-jsx';
8
+ import { parse, parseFragment, serialize } from 'parse5';
9
+ import path from 'path';
10
+ import { URL, pathToFileURL } from 'url';
11
+
12
+ const baseURL = pathToFileURL(`${process.cwd()}/`).href;
13
+ const jsxRegex = /\.(jsx)$/;
14
+
15
+ // TODO same hack as definitions
16
+ // https://github.com/ProjectEvergreen/wcc/discussions/74
17
+ let string;
18
+
19
+ // TODO move to a util
20
+ // https://github.com/ProjectEvergreen/wcc/discussions/74
21
+ function getParse(html) {
22
+ return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
23
+ ? parse
24
+ : parseFragment;
25
+ }
26
+
27
+ export function getParser(moduleURL) {
28
+ const isJSX = path.extname(moduleURL.pathname) === '.jsx';
29
+
30
+ if (!isJSX) {
31
+ return;
32
+ }
33
+
34
+ return {
35
+ parser: acorn.Parser.extend(jsx()),
36
+ config: {
37
+ // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
38
+ ...walk.base,
39
+ JSXElement: () => {}
40
+ }
41
+ };
42
+ }
43
+
44
+ function applyDomDepthSubstitutions(tree, currentDepth = 1, hasShadowRoot = false) {
45
+ try {
46
+ for (const node of tree.childNodes) {
47
+ const attrs = node.attrs;
48
+
49
+ // check for attributes
50
+ // and swap out __this__ with depthful parentElement chain
51
+ if (attrs && attrs.length > 0) {
52
+ for (const attr in attrs) {
53
+ const { value } = attrs[attr];
54
+
55
+ if (value.indexOf('__this__.') >= 0) {
56
+ const root = hasShadowRoot ? 'parentNode.host' : 'parentElement';
57
+
58
+ node.attrs[attr].value = value.replace(/__this__/g, `this${'.parentElement'.repeat(currentDepth - 1)}.${root}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ if (node.childNodes && node.childNodes.length > 0) {
64
+ applyDomDepthSubstitutions(node, currentDepth + 1, hasShadowRoot);
65
+ }
66
+ }
67
+ } catch (e) {
68
+ console.error(e);
69
+ }
70
+
71
+ return tree;
72
+ }
73
+
74
+ function parseJsxElement(element, moduleContents = '') {
75
+ try {
76
+ const { type } = element;
77
+
78
+ if (type === 'JSXElement') {
79
+ const { openingElement } = element;
80
+ const { attributes } = openingElement;
81
+ const tagName = openingElement.name.name;
82
+
83
+ string += `<${tagName}`;
84
+
85
+ for (const attribute of attributes) {
86
+ const { name } = attribute.name;
87
+
88
+ // handle events
89
+ if (name.startsWith('on')) {
90
+ const { value } = attribute;
91
+ const { expression } = value;
92
+
93
+ // onclick={this.increment}
94
+ if (value.type === 'JSXExpressionContainer') {
95
+ if (expression.type === 'MemberExpression') {
96
+ if (expression.object.type === 'ThisExpression') {
97
+ if (expression.property.type === 'Identifier') {
98
+ // we leave markers for `this` so we can replace it later while also NOT accidentally replacing
99
+ // legitimate uses of this that might be actual content / markup of the custom element
100
+ string += ` ${name}="__this__.${expression.property.name}()"`;
101
+ }
102
+ }
103
+ }
104
+
105
+ // onclick={() => this.deleteUser(user.id)}
106
+ // TODO onclick={(e) => { this.deleteUser(user.id) }}
107
+ // TODO onclick={(e) => { this.deleteUser(user.id) && this.logAction(user.id) }}
108
+ // https://github.com/ProjectEvergreen/wcc/issues/88
109
+ if (expression.type === 'ArrowFunctionExpression') {
110
+ if (expression.body && expression.body.type === 'CallExpression') {
111
+ const { start, end } = expression;
112
+ string += ` ${name}="${moduleContents.slice(start, end).replace(/this./g, '__this__.').replace('() => ', '')}"`;
113
+ }
114
+ }
115
+
116
+ if (expression.type === 'AssignmentExpression') {
117
+ const { left, right } = expression;
118
+
119
+ if (left.object.type === 'ThisExpression') {
120
+ if (left.property.type === 'Identifier') {
121
+ // very naive (fine grained?) reactivity
122
+ string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.render();"`;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ } else if (attribute.name.type === 'JSXIdentifier') {
128
+ // TODO is there any difference between an attribute for an event handler vs a normal attribute?
129
+ // Can all these be parsed using one function>
130
+ if (attribute.value) {
131
+ if (attribute.value.type === 'Literal') {
132
+ // xxx="yyy" >
133
+ string += ` ${name}="${attribute.value.value}"`;
134
+ } else if (attribute.value.type === 'JSXExpressionContainer') {
135
+ // xxx={allTodos.length} >
136
+ const { value } = attribute;
137
+ const { expression } = value;
138
+
139
+ if (expression.type === 'Identifier') {
140
+ string += ` ${name}=\$\{${expression.name}\}`;
141
+ }
142
+
143
+ if (expression.type === 'MemberExpression') {
144
+ if (expression.object.type === 'Identifier') {
145
+ if (expression.property.type === 'Identifier') {
146
+ string += ` ${name}=\$\{${expression.object.name}.${expression.property.name}\}`;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ } else {
152
+ // xxx >
153
+ string += ` ${name}`;
154
+ }
155
+ }
156
+ }
157
+
158
+ string += openingElement.selfClosing ? '/>' : '>';
159
+
160
+ if (element.children.length > 0) {
161
+ element.children.forEach(child => parseJsxElement(child, moduleContents));
162
+ }
163
+
164
+ string += `</${tagName}>`;
165
+ }
166
+
167
+ if (type === 'JSXText') {
168
+ string += element.raw;
169
+ }
170
+
171
+ if (type === 'JSXExpressionContainer') {
172
+ const { type } = element.expression;
173
+
174
+ if (type === 'Identifier') {
175
+ // You have {count} TODOs left to complete
176
+ string += `\$\{${element.expression.name}\}`;
177
+ } else if (type === 'MemberExpression') {
178
+ const { object } = element.expression.object;
179
+
180
+ // You have {this.todos.length} Todos left to complete
181
+ // https://github.com/ProjectEvergreen/wcc/issues/88
182
+ if (object && object.type === 'ThisExpression') {
183
+ // TODO ReferenceError: __this__ is not defined
184
+ // string += `\$\{__this__.${element.expression.object.property.name}.${element.expression.property.name}\}`;
185
+ } else {
186
+ // const { todos } = this;
187
+ // ....
188
+ // You have {todos.length} Todos left to complete
189
+ string += `\$\{${element.expression.object.name}.${element.expression.property.name}\}`;
190
+ }
191
+ }
192
+ }
193
+ } catch (e) {
194
+ console.error(e);
195
+ }
196
+
197
+ return string;
198
+ }
199
+
200
+ export function parseJsx(moduleURL) {
201
+ const moduleContents = fs.readFileSync(moduleURL, 'utf-8');
202
+ string = '';
203
+
204
+ const tree = acorn.Parser.extend(jsx()).parse(moduleContents, {
205
+ ecmaVersion: 'latest',
206
+ sourceType: 'module'
207
+ });
208
+
209
+ walk.simple(tree, {
210
+ ClassDeclaration(node) {
211
+ if (node.superClass.name === 'HTMLElement') {
212
+ const hasShadowRoot = moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0;
213
+
214
+ for (const n1 of node.body.body) {
215
+ if (n1.type === 'MethodDefinition' && n1.key.name === 'render') {
216
+ for (const n2 in n1.value.body.body) {
217
+ const n = n1.value.body.body[n2];
218
+
219
+ if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') {
220
+ const html = parseJsxElement(n.argument, moduleContents);
221
+ const elementTree = getParse(html)(html);
222
+ const elementRoot = hasShadowRoot ? 'this.shadowRoot' : 'this';
223
+
224
+ applyDomDepthSubstitutions(elementTree, undefined, hasShadowRoot);
225
+
226
+ const finalHtml = serialize(elementTree);
227
+ const transformed = acorn.parse(`${elementRoot}.innerHTML = \`${finalHtml}\`;`, {
228
+ ecmaVersion: 'latest',
229
+ sourceType: 'module'
230
+ });
231
+
232
+ n1.value.body.body[n2] = transformed;
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }, {
240
+ // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
241
+ ...walk.base,
242
+ JSXElement: () => {}
243
+ });
244
+
245
+ return tree;
246
+ }
247
+
248
+ // --------------
249
+
250
+ export function resolve(specifier, context, defaultResolve) {
251
+ const { parentURL = baseURL } = context;
252
+
253
+ if (jsxRegex.test(specifier)) {
254
+ return {
255
+ url: new URL(specifier, parentURL).href
256
+ };
257
+ }
258
+
259
+ return defaultResolve(specifier, context, defaultResolve);
260
+ }
261
+
262
+ export async function load(url, context, defaultLoad) {
263
+ if (jsxRegex.test(url)) {
264
+ const jsFromJsx = parseJsx(new URL(url));
265
+
266
+ return {
267
+ format: 'module',
268
+ source: escodegen.generate(jsFromJsx)
269
+ };
270
+ }
271
+
272
+ return defaultLoad(url, context, defaultLoad);
273
+ }