wc-compiler 0.13.0 → 0.15.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.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,9 +32,9 @@
32
32
  "docs:dev": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html,jsx ./build.js\" \"http-server ./dist --open\"",
33
33
  "docs:build": "node ./build.js",
34
34
  "docs:serve": "npm run clean && npm run docs:build && http-server ./dist --open",
35
- "sandbox": "npm run clean && concurrently \"nodemon --loader ./test-exp-loader.js --watch src --watch sandbox -e js,md,css,html,jsx ./sandbox.js\" \"http-server ./dist --open\" \"livereload ./dist\"",
35
+ "sandbox": "npm run clean && concurrently \"nodemon --loader ./test-exp-loader.js --watch src --watch sandbox -e js,md,css,html,jsx,ts ./sandbox.js\" \"http-server ./dist --open\" \"livereload ./dist\"",
36
36
  "start": "npm run docs:serve",
37
- "test": "mocha --exclude \"./test/cases/jsx*/**\" --exclude \"./test/cases/custom-extension/**\" \"./test/**/**/*.spec.js\"",
37
+ "test": "mocha --exclude \"./test/cases/jsx*/**\" --exclude \"./test/cases/ts*/**\" --exclude \"./test/cases/custom-extension/**\" \"./test/**/**/*.spec.js\"",
38
38
  "test:exp": "c8 node --loader ./test-exp-loader.js ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
39
39
  "test:tdd": "npm run test -- --watch",
40
40
  "test:tdd:exp": "npm run test:exp -- --watch",
@@ -47,7 +47,8 @@
47
47
  "acorn": "^8.7.0",
48
48
  "acorn-import-attributes": "^1.9.5",
49
49
  "acorn-walk": "^8.2.0",
50
- "parse5": "^6.0.1"
50
+ "parse5": "^6.0.1",
51
+ "sucrase": "^3.35.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@babel/core": "^7.24.4",
package/src/jsx-loader.js CHANGED
@@ -8,6 +8,7 @@ import jsx from '@projectevergreen/acorn-jsx-esm';
8
8
  import { parse, parseFragment, serialize } from 'parse5';
9
9
  // Need an acorn plugin for now - https://github.com/ProjectEvergreen/greenwood/issues/1218
10
10
  import { importAttributes } from 'acorn-import-attributes';
11
+ import { transform } from 'sucrase';
11
12
 
12
13
  const jsxRegex = /\.(jsx)$/;
13
14
 
@@ -232,13 +233,17 @@ function findThisReferences(context, statement) {
232
233
 
233
234
  export function parseJsx(moduleURL) {
234
235
  const moduleContents = fs.readFileSync(moduleURL, 'utf-8');
236
+ const result = transform(moduleContents, {
237
+ transforms: ['typescript', 'jsx'],
238
+ jsxRuntime: 'preserve'
239
+ });
235
240
  // would be nice if we could do this instead, so we could know ahead of time
236
241
  // const { inferredObservability } = await import(moduleURL);
237
242
  // however, this requires making parseJsx async, but WCC acorn walking is done sync
238
243
  const hasOwnObservedAttributes = undefined;
239
244
  let inferredObservability = false;
240
245
  let observedAttributes = [];
241
- let tree = acorn.Parser.extend(jsx(), importAttributes).parse(moduleContents, {
246
+ let tree = acorn.Parser.extend(jsx(), importAttributes).parse(result.code, {
242
247
  ecmaVersion: 'latest',
243
248
  sourceType: 'module'
244
249
  });
@@ -0,0 +1,32 @@
1
+ import fs from 'fs/promises';
2
+ import { transform } from 'sucrase';
3
+
4
+ const tsRegex = /\.(ts)$/;
5
+
6
+ export function resolve(specifier, context, defaultResolve) {
7
+ const { parentURL } = context;
8
+
9
+ if (tsRegex.test(specifier)) {
10
+ return {
11
+ url: new URL(specifier, parentURL).href,
12
+ shortCircuit: true
13
+ };
14
+ }
15
+
16
+ return defaultResolve(specifier, context, defaultResolve);
17
+ }
18
+
19
+ export async function load(url, context, defaultLoad) {
20
+ if (tsRegex.test(url)) {
21
+ const contents = await fs.readFile(new URL(url), 'utf-8');
22
+ const result = transform(contents, { transforms: ['typescript'] });
23
+
24
+ return {
25
+ format: 'module',
26
+ shortCircuit: true,
27
+ source: result.code
28
+ };
29
+ }
30
+
31
+ return defaultLoad(url, context, defaultLoad);
32
+ }
package/src/wcc.js CHANGED
@@ -9,8 +9,27 @@ import { getParser, parseJsx } from './jsx-loader.js';
9
9
  import { parse, parseFragment, serialize } from 'parse5';
10
10
  // Need an acorn plugin for now - https://github.com/ProjectEvergreen/greenwood/issues/1218
11
11
  import { importAttributes } from 'acorn-import-attributes';
12
+ import { transform } from 'sucrase';
12
13
  import fs from 'fs';
13
14
 
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
+
14
33
  function getParse(html) {
15
34
  return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
16
35
  ? parse
@@ -32,17 +51,26 @@ async function renderComponentRoots(tree, definitions) {
32
51
 
33
52
  if (definitions[tagName]) {
34
53
  const { moduleURL } = definitions[tagName];
35
- const elementInstance = await initializeCustomElement(moduleURL, tagName, node.attrs, definitions);
36
- const elementHtml = elementInstance.shadowRoot
37
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
38
- : elementInstance.innerHTML;
39
- const elementTree = parseFragment(elementHtml);
40
-
41
- node.childNodes = node.childNodes.length === 0
42
- ? elementTree.childNodes
43
- : [...elementTree.childNodes, ...node.childNodes];
54
+ const elementInstance = await initializeCustomElement(moduleURL, tagName, node, definitions);
55
+
56
+ if (elementInstance) {
57
+ 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;
69
+ } else {
70
+ console.warn(`WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`);
71
+ }
44
72
  } else {
45
- console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it yet.`);
73
+ console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it.`);
46
74
  }
47
75
  }
48
76
 
@@ -61,6 +89,10 @@ async function renderComponentRoots(tree, definitions) {
61
89
 
62
90
  function registerDependencies(moduleURL, definitions, depth = 0) {
63
91
  const moduleContents = fs.readFileSync(moduleURL, 'utf-8');
92
+ const result = transform(moduleContents, {
93
+ transforms: ['typescript', 'jsx'],
94
+ jsxRuntime: 'preserve'
95
+ });
64
96
  const nextDepth = depth += 1;
65
97
  const customParser = getParser(moduleURL);
66
98
  const parser = customParser ? customParser.parser : acorn.Parser;
@@ -68,7 +100,7 @@ function registerDependencies(moduleURL, definitions, depth = 0) {
68
100
  ...walk.base
69
101
  };
70
102
 
71
- walk.simple(parser.extend(importAttributes).parse(moduleContents, {
103
+ walk.simple(parser.extend(importAttributes).parse(result.code, {
72
104
  ecmaVersion: 'latest',
73
105
  sourceType: 'module'
74
106
  }), {
@@ -77,8 +109,9 @@ function registerDependencies(moduleURL, definitions, depth = 0) {
77
109
  const isBareSpecifier = specifier.indexOf('.') !== 0 && specifier.indexOf('/') !== 0;
78
110
  const extension = specifier.split('.').pop();
79
111
 
80
- // TODO would like to decouple .jsx from the core, ideally
81
- if (!isBareSpecifier && ['js', 'jsx'].includes(extension)) {
112
+ // would like to decouple .jsx from the core, ideally
113
+ // https://github.com/ProjectEvergreen/wcc/issues/122
114
+ if (!isBareSpecifier && ['js', 'jsx', 'ts'].includes(extension)) {
82
115
  const dependencyModuleURL = new URL(node.source.value, moduleURL);
83
116
 
84
117
  registerDependencies(dependencyModuleURL, definitions, nextDepth);
@@ -107,6 +140,10 @@ function registerDependencies(moduleURL, definitions, depth = 0) {
107
140
 
108
141
  async function getTagName(moduleURL) {
109
142
  const moduleContents = await fs.promises.readFile(moduleURL, 'utf-8');
143
+ const result = transform(moduleContents, {
144
+ transforms: ['typescript', 'jsx'],
145
+ jsxRuntime: 'preserve'
146
+ });
110
147
  const customParser = getParser(moduleURL);
111
148
  const parser = customParser ? customParser.parser : acorn.Parser;
112
149
  const config = customParser ? customParser.config : {
@@ -114,7 +151,7 @@ async function getTagName(moduleURL) {
114
151
  };
115
152
  let tagName;
116
153
 
117
- walk.simple(parser.extend(importAttributes).parse(moduleContents, {
154
+ walk.simple(parser.extend(importAttributes).parse(result.code, {
118
155
  ecmaVersion: 'latest',
119
156
  sourceType: 'module'
120
157
  }), {
@@ -128,7 +165,41 @@ async function getTagName(moduleURL) {
128
165
  return tagName;
129
166
  }
130
167
 
131
- async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry, props = {}) {
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
+ async function initializeCustomElement(elementURL, tagName, node = {}, definitions = [], isEntry, props = {}) {
201
+ const { attrs = [], childNodes = [] } = node;
202
+
132
203
  if (!tagName) {
133
204
  const depth = isEntry ? 1 : 0;
134
205
  registerDependencies(elementURL, definitions, depth);
@@ -148,6 +219,9 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
148
219
  if (element) {
149
220
  const elementInstance = new element(data); // eslint-disable-line new-cap
150
221
 
222
+ // support for HTML (Light DOM) Web Components
223
+ elementInstance.innerHTML = renderLightDomChildren(childNodes);
224
+
151
225
  attrs.forEach((attr) => {
152
226
  elementInstance.setAttribute(attr.name, attr.value);
153
227
 
@@ -197,7 +271,7 @@ async function renderFromHTML(html, elements = []) {
197
271
  const definitions = [];
198
272
 
199
273
  for (const url of elements) {
200
- await initializeCustomElement(url, undefined, undefined, definitions, true);
274
+ registerDependencies(url, definitions, 1);
201
275
  }
202
276
 
203
277
  const elementTree = getParse(html)(html);