wc-compiler 0.5.0 → 0.6.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.
- package/README.md +1 -1
- package/dist/wcc.dist.cjs +14008 -74
- package/package.json +10 -5
- package/src/dom-shim.js +55 -58
- package/src/jsx-loader.js +273 -0
- package/src/wcc.js +40 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wc-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Experimental native Web Components compiler.",
|
|
5
5
|
"main": "src/wcc.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,24 +18,29 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"clean": "rimraf ./dist",
|
|
21
|
-
"lint": "eslint \"*.*js\" \"./src/**/**/*.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
25
|
"start": "npm run develop",
|
|
26
|
-
"test": "
|
|
27
|
-
"test:
|
|
26
|
+
"test": "mocha --exclude \"./test/cases/jsx/**\" --exclude \"./test/cases/custom-extension/**\" \"./test/**/**/*.spec.js\"",
|
|
27
|
+
"test:exp": "c8 node --experimental-loader ./test-exp-loader.js ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
|
|
28
|
+
"test:tdd": "npm run test -- --watch",
|
|
29
|
+
"test:tdd:exp": "npm run test:exp -- --watch",
|
|
28
30
|
"dist": "rollup -c rollup.config.js",
|
|
29
31
|
"prepublishOnly": "npm run clean && npm run dist"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"acorn": "^8.7.0",
|
|
35
|
+
"acorn-jsx": "^5.3.2",
|
|
33
36
|
"acorn-walk": "^8.2.0",
|
|
37
|
+
"escodegen": "^2.0.0",
|
|
34
38
|
"parse5": "^6.0.1"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@mapbox/rehype-prism": "^0.8.0",
|
|
38
42
|
"@rollup/plugin-commonjs": "^22.0.0",
|
|
43
|
+
"@rollup/plugin-json": "^4.1.0",
|
|
39
44
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
|
40
45
|
"c8": "^7.11.2",
|
|
41
46
|
"chai": "^4.3.6",
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
+
}
|
package/src/wcc.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
/* eslint-disable max-depth */
|
|
1
2
|
// this must come first
|
|
2
3
|
import './dom-shim.js';
|
|
3
4
|
|
|
4
5
|
import * as acorn from 'acorn';
|
|
5
6
|
import * as walk from 'acorn-walk';
|
|
7
|
+
import escodegen from 'escodegen';
|
|
8
|
+
import { getParser, parseJsx } from './jsx-loader.js';
|
|
6
9
|
import { parse, parseFragment, serialize } from 'parse5';
|
|
7
10
|
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
8
12
|
|
|
9
13
|
function getParse(html) {
|
|
10
14
|
return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
|
|
@@ -54,10 +58,16 @@ async function renderComponentRoots(tree, definitions) {
|
|
|
54
58
|
return tree;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
function registerDependencies(moduleURL, definitions) {
|
|
61
|
+
function registerDependencies(moduleURL, definitions, depth = 0) {
|
|
58
62
|
const moduleContents = fs.readFileSync(moduleURL, 'utf-8');
|
|
63
|
+
const nextDepth = depth += 1;
|
|
64
|
+
const customParser = getParser(moduleURL);
|
|
65
|
+
const parser = customParser ? customParser.parser : acorn;
|
|
66
|
+
const config = customParser ? customParser.config : {
|
|
67
|
+
...walk.base
|
|
68
|
+
};
|
|
59
69
|
|
|
60
|
-
walk.simple(
|
|
70
|
+
walk.simple(parser.parse(moduleContents, {
|
|
61
71
|
ecmaVersion: 'latest',
|
|
62
72
|
sourceType: 'module'
|
|
63
73
|
}), {
|
|
@@ -65,47 +75,62 @@ function registerDependencies(moduleURL, definitions) {
|
|
|
65
75
|
const specifier = node.source.value;
|
|
66
76
|
const isBareSpecifier = specifier.indexOf('.') !== 0 && specifier.indexOf('/') !== 0;
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
// TODO would like to decouple .jsx from the core, ideally
|
|
79
|
+
if (!isBareSpecifier && ['.js', '.jsx'].includes(path.extname(specifier))) {
|
|
69
80
|
const dependencyModuleURL = new URL(node.source.value, moduleURL);
|
|
70
81
|
|
|
71
|
-
registerDependencies(dependencyModuleURL, definitions);
|
|
82
|
+
registerDependencies(dependencyModuleURL, definitions, nextDepth);
|
|
72
83
|
}
|
|
73
84
|
},
|
|
74
85
|
ExpressionStatement(node) {
|
|
75
86
|
if (isCustomElementDefinitionNode(node)) {
|
|
76
87
|
const { arguments: args } = node.expression;
|
|
77
|
-
const tagName = args[0].
|
|
88
|
+
const tagName = args[0].type === 'Literal'
|
|
89
|
+
? args[0].value // single and double quotes
|
|
90
|
+
: args[0].quasis[0].value.raw; // template literal
|
|
91
|
+
const tree = parseJsx(moduleURL);
|
|
92
|
+
const isEntry = nextDepth - 1 === 1;
|
|
78
93
|
|
|
79
94
|
definitions[tagName] = {
|
|
80
95
|
instanceName: args[1].name,
|
|
81
|
-
moduleURL
|
|
96
|
+
moduleURL,
|
|
97
|
+
source: escodegen.generate(tree),
|
|
98
|
+
url: moduleURL,
|
|
99
|
+
isEntry
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
|
-
});
|
|
103
|
+
}, config);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
async function getTagName(moduleURL) {
|
|
89
107
|
const moduleContents = await fs.promises.readFile(moduleURL, 'utf-8');
|
|
108
|
+
const customParser = getParser(moduleURL);
|
|
109
|
+
const parser = customParser ? customParser.parser : acorn;
|
|
110
|
+
const config = customParser ? customParser.config : {
|
|
111
|
+
...walk.base
|
|
112
|
+
};
|
|
90
113
|
let tagName;
|
|
91
114
|
|
|
92
|
-
walk.simple(
|
|
115
|
+
walk.simple(parser.parse(moduleContents, {
|
|
93
116
|
ecmaVersion: 'latest',
|
|
94
117
|
sourceType: 'module'
|
|
95
118
|
}), {
|
|
96
119
|
ExpressionStatement(node) {
|
|
97
120
|
if (isCustomElementDefinitionNode(node)) {
|
|
98
|
-
|
|
99
121
|
tagName = node.expression.arguments[0].value;
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
|
-
});
|
|
124
|
+
}, config);
|
|
103
125
|
|
|
104
126
|
return tagName;
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = []) {
|
|
108
|
-
|
|
129
|
+
async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry) {
|
|
130
|
+
if (!tagName) {
|
|
131
|
+
const depth = isEntry ? 1 : 0;
|
|
132
|
+
registerDependencies(elementURL, definitions, depth);
|
|
133
|
+
}
|
|
109
134
|
|
|
110
135
|
// https://github.com/ProjectEvergreen/wcc/pull/67/files#r902061804
|
|
111
136
|
const { pathname } = elementURL;
|
|
@@ -132,7 +157,8 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
|
|
|
132
157
|
async function renderToString(elementURL) {
|
|
133
158
|
const definitions = [];
|
|
134
159
|
const elementTagName = await getTagName(elementURL);
|
|
135
|
-
const
|
|
160
|
+
const isEntry = !!elementTagName;
|
|
161
|
+
const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry);
|
|
136
162
|
|
|
137
163
|
const elementHtml = elementInstance.shadowRoot
|
|
138
164
|
? elementInstance.getInnerHTML({ includeShadowRoots: true })
|
|
@@ -156,7 +182,7 @@ async function renderFromHTML(html, elements = []) {
|
|
|
156
182
|
const definitions = [];
|
|
157
183
|
|
|
158
184
|
for (const url of elements) {
|
|
159
|
-
await initializeCustomElement(url, undefined, undefined, definitions);
|
|
185
|
+
await initializeCustomElement(url, undefined, undefined, definitions, true);
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
const elementTree = getParse(html)(html);
|