wc-compiler 0.15.1 → 0.17.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/README.md CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  [![Netlify Status](https://api.netlify.com/api/v1/badges/e718eac2-b3bc-4986-8569-49706a430beb/deploy-status)](https://app.netlify.com/sites/merry-caramel-524e61/deploys)
6
6
  [![GitHub release](https://img.shields.io/github/tag/ProjectEvergreen/wcc.svg)](https://github.com/ProjectEvergreen/wcc/tags)
7
- ![GitHub Actions status](https://github.com/ProjectEvergreen/wcc/workflows/Master%20Integration/badge.svg)
8
7
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ProjectEvergreen/wcc/master/LICENSE.md)
8
+ [![NodeJS compatibility](https://img.shields.io/node/v/wc-compiler.svg)](https://nodejs.org/en/about/previous-releases")
9
+ [![Discord Chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://www.greenwoodjs.dev/discord/)
9
10
 
10
11
  > _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢
11
12
 
@@ -73,13 +74,6 @@
73
74
  $ npm install wc-compiler --save-dev
74
75
  ```
75
76
 
76
- ### CommonJS
77
-
78
- If you need CommonJS support, a separate pre-bundled (with Rollup) distribution of **WCC** is available at _dist/wcc.dist.cjs_. Example:
79
- ```js
80
- const { renderToString } = require('wc-compiler/dist/wcc.dist.cjs');
81
- ```
82
-
83
77
  ## Documentation
84
78
 
85
79
  See our [website](https://merry-caramel-524e61.netlify.app/) for API docs and examples.
package/package.json CHANGED
@@ -1,13 +1,22 @@
1
1
  {
2
2
  "name": "wc-compiler",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/ProjectEvergreen/wcc.git"
8
8
  },
9
- "main": "src/wcc.js",
10
9
  "type": "module",
10
+ "main": "src/wcc.js",
11
+ "types": "./src/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./src/wcc.js",
15
+ "types": "./src/index.d.ts"
16
+ },
17
+ "./register": "./src/register.js",
18
+ "./src/jsx-loader.js": "./src/jsx-loader.js"
19
+ },
11
20
  "author": "Owen Buckley <owen@thegreenhouse.io>",
12
21
  "keywords": [
13
22
  "Web Components",
@@ -17,53 +26,51 @@
17
26
  ],
18
27
  "license": "MIT",
19
28
  "engines": {
20
- "node": ">=14"
29
+ "node": ">=18"
21
30
  },
22
31
  "files": [
23
- "src/",
24
- "dist/wcc.dist.cjs"
32
+ "src/"
25
33
  ],
26
34
  "publishConfig": {
27
35
  "access": "public"
28
36
  },
29
37
  "scripts": {
30
38
  "clean": "rimraf ./dist",
31
- "lint": "eslint --ignore-pattern \"*.json\" \"*.*js\" \"./src/**/**/*.js*\" \"./sandbox/**/**/*.js*\" \"./docs/**/*.md\" \"./test/**/**/*.js*\"",
39
+ "lint": "eslint",
40
+ "lint:types": "tsc --project tsconfig.json",
32
41
  "docs:dev": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html,jsx ./build.js\" \"http-server ./dist --open\"",
33
42
  "docs:build": "node ./build.js",
34
43
  "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,ts ./sandbox.js\" \"http-server ./dist --open\" \"livereload ./dist\"",
44
+ "sandbox": "npm run clean && concurrently \"nodemon --loader ./test-loader.js --watch src --watch sandbox -e js,md,css,html,jsx,ts ./sandbox.js\" \"http-server ./dist --open\" \"livereload ./dist\"",
36
45
  "start": "npm run docs:serve",
37
46
  "test": "mocha --exclude \"./test/cases/jsx*/**\" --exclude \"./test/cases/ts*/**\" --exclude \"./test/cases/custom-extension/**\" \"./test/**/**/*.spec.js\"",
38
- "test:exp": "c8 node --loader ./test-exp-loader.js ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
47
+ "test:jsx": "c8 node --import ./test-register.js --experimental-strip-types ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
39
48
  "test:tdd": "npm run test -- --watch",
40
- "test:tdd:exp": "npm run test:exp -- --watch",
41
- "dist": "rollup -c rollup.config.js",
42
- "prepublishOnly": "npm run clean && npm run dist"
49
+ "test:tdd:jsx": "npm run test:jsx -- --watch"
43
50
  },
44
51
  "dependencies": {
45
52
  "@projectevergreen/acorn-jsx-esm": "~0.1.0",
46
53
  "acorn": "^8.14.0",
47
54
  "acorn-walk": "^8.3.4",
48
55
  "astring": "^1.9.0",
49
- "parse5": "^6.0.1",
56
+ "parse5": "^7.2.1",
50
57
  "sucrase": "^3.35.0"
51
58
  },
52
59
  "devDependencies": {
53
60
  "@babel/core": "^7.24.4",
54
- "@babel/eslint-parser": "^7.24.1",
55
- "@babel/plugin-syntax-import-assertions": "^7.24.1",
56
- "@babel/preset-react": "^7.24.1",
61
+ "@babel/eslint-parser": "^7.25.7",
62
+ "@babel/plugin-syntax-import-assertions": "^7.25.7",
63
+ "@eslint/js": "^9.11.1",
57
64
  "@ls-lint/ls-lint": "^1.10.0",
58
65
  "@mapbox/rehype-prism": "^0.8.0",
59
- "@rollup/plugin-commonjs": "^28.0.0",
60
- "@rollup/plugin-node-resolve": "^15.2.3",
66
+ "@types/mocha": "^10.0.10",
67
+ "@types/node": "^22.13.4",
61
68
  "c8": "^7.11.2",
62
69
  "chai": "^4.3.6",
63
70
  "concurrently": "^7.1.0",
64
- "eslint": "^8.14.0",
65
- "eslint-plugin-markdown": "^3.0.0",
71
+ "eslint": "^9.11.1",
66
72
  "eslint-plugin-no-only-tests": "^2.6.0",
73
+ "globals": "^15.10.0",
67
74
  "http-server": "^14.1.0",
68
75
  "jsdom": "^19.0.0",
69
76
  "livereload": "^0.9.3",
@@ -78,8 +85,8 @@
78
85
  "remark-rehype": "^10.1.0",
79
86
  "remark-toc": "^8.0.1",
80
87
  "rimraf": "^3.0.2",
81
- "rollup": "^4.26.0",
82
88
  "simple.css": "^0.1.3",
89
+ "typescript": "^5.8.2",
83
90
  "unified": "^10.1.2"
84
91
  }
85
92
  }
package/src/dom-shim.js CHANGED
@@ -1,3 +1,55 @@
1
+ // @ts-nocheck
2
+ import { parse, parseFragment, serialize } from 'parse5';
3
+
4
+ export function getParse(html) {
5
+ return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
6
+ ? parse
7
+ : parseFragment;
8
+ }
9
+
10
+ function isShadowRoot(element) {
11
+ return Object.getPrototypeOf(element).constructor.name === 'ShadowRoot';
12
+ }
13
+
14
+ function deepClone(obj, map = new WeakMap()) {
15
+ if (obj === null || typeof obj !== 'object') {
16
+ return obj;
17
+ }
18
+
19
+ if (typeof obj === 'function') {
20
+ const clonedFn = obj.bind({});
21
+ Object.assign(clonedFn, obj);
22
+ return clonedFn;
23
+ }
24
+
25
+ if (map.has(obj)) {
26
+ return map.get(obj);
27
+ }
28
+
29
+ const result = Array.isArray(obj) ? [] : {};
30
+ map.set(obj, result);
31
+
32
+ for (const key of Object.keys(obj)) {
33
+ result[key] = deepClone(obj[key], map);
34
+ }
35
+
36
+ return result;
37
+ }
38
+
39
+ // Creates an empty parse5 element without the parse5 overhead resulting in better performance
40
+ function getParse5ElementDefaults(element, tagName) {
41
+ return {
42
+ addEventListener: noop,
43
+ attrs: [],
44
+ parentNode: element.parentNode,
45
+ childNodes: [],
46
+ nodeName: tagName,
47
+ tagName: tagName,
48
+ namespaceURI: 'http://www.w3.org/1999/xhtml',
49
+ ...(tagName === 'template' ? { content: { nodeName: '#document-fragment', childNodes: [] } } : {})
50
+ };
51
+ }
52
+
1
53
  function noop() { }
2
54
 
3
55
  // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
@@ -19,13 +71,81 @@ class EventTarget {
19
71
  // EventTarget <- Node
20
72
  // TODO should be an interface?
21
73
  class Node extends EventTarget {
22
- // eslint-disable-next-line
74
+ constructor() {
75
+ super();
76
+ // Parse5 properties
77
+ this.attrs = [];
78
+ this.parentNode = null;
79
+ this.childNodes = [];
80
+ this.nodeName = '';
81
+ }
82
+
23
83
  cloneNode(deep) {
24
- return this;
84
+ return deep ? deepClone(this) : Object.assign({}, this);
25
85
  }
26
86
 
27
87
  appendChild(node) {
28
- this.innerHTML = this.innerHTML ? this.innerHTML += node.innerHTML : node.innerHTML;
88
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
89
+
90
+ if (node.parentNode) {
91
+ node.parentNode?.removeChild?.(node);
92
+ }
93
+
94
+ if (node.nodeName === 'template') {
95
+ if (isShadowRoot(this) && this.mode) {
96
+ node.attrs = [{ name: 'shadowrootmode', value: this.mode }];
97
+ childNodes.push(node);
98
+ node.parentNode = this;
99
+ } else {
100
+ this.childNodes = [...this.childNodes, ...node.content.childNodes];
101
+ }
102
+ } else if (node instanceof DocumentFragment) {
103
+ this.childNodes = [...this.childNodes, ...node.childNodes];
104
+ } else {
105
+ childNodes.push(node);
106
+ node.parentNode = this;
107
+ }
108
+
109
+ return node;
110
+ }
111
+
112
+ removeChild(node) {
113
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
114
+ if (!childNodes || !childNodes.length) {
115
+ return null;
116
+ }
117
+
118
+ const index = childNodes.indexOf(node);
119
+ if (index === -1) {
120
+ return null;
121
+ }
122
+
123
+ childNodes.splice(index, 1);
124
+ node.parentNode = null;
125
+
126
+ return node;
127
+ }
128
+
129
+ get textContent() {
130
+ if (this.nodeName === '#text') {
131
+ return this.value || '';
132
+ }
133
+
134
+ return this.childNodes
135
+ .map((child) => child.nodeName === '#text' ? child.value : child.textContent)
136
+ .join('');
137
+ }
138
+
139
+ set textContent(value) {
140
+ this.childNodes = [];
141
+
142
+ if (value) {
143
+ const textNode = new Node();
144
+ textNode.nodeName = '#text';
145
+ textNode.value = value;
146
+ textNode.parentNode = this;
147
+ this.childNodes.push(textNode);
148
+ }
29
149
  }
30
150
  }
31
151
 
@@ -34,33 +154,44 @@ class Node extends EventTarget {
34
154
  class Element extends Node {
35
155
  constructor() {
36
156
  super();
37
- this.shadowRoot = null;
38
- this.innerHTML = '';
39
- this.attributes = {};
40
157
  }
41
158
 
42
159
  attachShadow(options) {
43
160
  this.shadowRoot = new ShadowRoot(options);
44
-
161
+ this.shadowRoot.parentNode = this;
45
162
  return this.shadowRoot;
46
163
  }
47
164
 
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;
165
+ getHTML({ serializableShadowRoots = false }) {
166
+ return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable ? this.shadowRoot.innerHTML : '';
52
167
  }
53
168
 
54
- setAttribute(name, value) {
55
- this.attributes[name] = value;
169
+ get innerHTML() {
170
+ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
171
+ return childNodes ? serialize({ childNodes }) : '';
56
172
  }
57
173
 
58
- getAttribute(name) {
59
- return this.attributes[name];
174
+ set innerHTML(html) {
175
+ (this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes;
60
176
  }
61
177
 
62
178
  hasAttribute(name) {
63
- return !!this.attributes[name];
179
+ return this.attrs.some((attr) => attr.name === name);
180
+ }
181
+
182
+ getAttribute(name) {
183
+ const attr = this.attrs.find((attr) => attr.name === name);
184
+ return attr ? attr.value : null;
185
+ }
186
+
187
+ setAttribute(name, value) {
188
+ const attr = this.attrs?.find((attr) => attr.name === name);
189
+
190
+ if (attr) {
191
+ attr.value = value;
192
+ } else {
193
+ this.attrs?.push({ name, value });
194
+ }
64
195
  }
65
196
  }
66
197
 
@@ -75,7 +206,7 @@ class Document extends Node {
75
206
  return new HTMLTemplateElement();
76
207
 
77
208
  default:
78
- return new HTMLElement();
209
+ return new HTMLElement(tagName);
79
210
 
80
211
  }
81
212
  }
@@ -88,6 +219,10 @@ class Document extends Node {
88
219
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
89
220
  // EventTarget <- Node <- Element <- HTMLElement
90
221
  class HTMLElement extends Element {
222
+ constructor(tagName) {
223
+ super();
224
+ Object.assign(this, getParse5ElementDefaults(this, tagName));
225
+ }
91
226
  connectedCallback() { }
92
227
  }
93
228
 
@@ -100,9 +235,18 @@ class DocumentFragment extends Node { }
100
235
  class ShadowRoot extends DocumentFragment {
101
236
  constructor(options) {
102
237
  super();
103
- this.mode = options.mode || 'closed';
238
+ this.mode = options.mode ?? 'closed';
239
+ this.serializable = options.serializable ?? false;
104
240
  this.adoptedStyleSheets = [];
105
241
  }
242
+
243
+ get innerHTML() {
244
+ return this.childNodes?.[0]?.content?.childNodes ? serialize({ childNodes: this.childNodes[0].content.childNodes }) : '';
245
+ }
246
+
247
+ set innerHTML(html) {
248
+ this.childNodes = getParse(html)(`<template shadowrootmode="${this.mode}">${html}</template>`).childNodes;
249
+ }
106
250
  }
107
251
 
108
252
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement
@@ -110,22 +254,11 @@ class ShadowRoot extends DocumentFragment {
110
254
  class HTMLTemplateElement extends HTMLElement {
111
255
  constructor() {
112
256
  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;
257
+ // Gets element defaults for template element instead of parsing a
258
+ // <template></template> with parse5. Results in better performance
259
+ // when creating templates
260
+ Object.assign(this, getParse5ElementDefaults(this, 'template'));
261
+ this.content.cloneNode = this.cloneNode.bind(this);
129
262
  }
130
263
  }
131
264
 
@@ -156,4 +289,5 @@ globalThis.addEventListener = globalThis.addEventListener ?? noop;
156
289
  globalThis.document = globalThis.document ?? new Document();
157
290
  globalThis.customElements = globalThis.customElements ?? new CustomElementsRegistry();
158
291
  globalThis.HTMLElement = globalThis.HTMLElement ?? HTMLElement;
292
+ globalThis.DocumentFragment = globalThis.DocumentFragment ?? DocumentFragment;
159
293
  globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? CSSStyleSheet;
package/src/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type Metadata = {
2
+ [key: string]: {
3
+ instanceName: string;
4
+ moduleURL: URL;
5
+ isEntry: boolean
6
+ }
7
+ }
8
+
9
+ export type renderToString = (elementURL: URL, wrappingEntryTag?: boolean, props?: any) => Promise<{
10
+ html: string;
11
+ metadata: Metadata
12
+ }>
13
+
14
+ export type renderFromHTML = (html: string, elementURLs: URL[]) => Promise<{
15
+ html: string;
16
+ metadata: Metadata
17
+ }>
18
+
19
+ declare module "wc-compiler" { }
package/src/jsx-loader.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable max-depth, complexity */
2
1
  // https://nodejs.org/api/esm.html#esm_loaders
3
2
  import * as acorn from 'acorn';
4
3
  import * as walk from 'acorn-walk';
@@ -6,6 +5,8 @@ import { generate } from 'astring';
6
5
  import fs from 'fs';
7
6
  // ideally we can eventually adopt an ESM compatible version of this plugin
8
7
  // https://github.com/acornjs/acorn-jsx/issues/112
8
+ // @ts-ignore
9
+ // but it does have a default export???
9
10
  import jsx from '@projectevergreen/acorn-jsx-esm';
10
11
  import { parse, parseFragment, serialize } from 'parse5';
11
12
  import { transform } from 'sucrase';
@@ -138,13 +139,13 @@ function parseJsxElement(element, moduleContents = '') {
138
139
  const { expression } = value;
139
140
 
140
141
  if (expression.type === 'Identifier') {
141
- string += ` ${name}=\$\{${expression.name}\}`;
142
+ string += ` ${name}=$\{${expression.name}}`;
142
143
  }
143
144
 
144
145
  if (expression.type === 'MemberExpression') {
145
146
  if (expression.object.type === 'Identifier') {
146
147
  if (expression.property.type === 'Identifier') {
147
- string += ` ${name}=\$\{${expression.object.name}.${expression.property.name}\}`;
148
+ string += ` ${name}=$\{${expression.object.name}.${expression.property.name}}`;
148
149
  }
149
150
  }
150
151
  }
@@ -174,7 +175,7 @@ function parseJsxElement(element, moduleContents = '') {
174
175
 
175
176
  if (type === 'Identifier') {
176
177
  // You have {count} TODOs left to complete
177
- string += `\$\{${element.expression.name}\}`;
178
+ string += `$\{${element.expression.name}}`;
178
179
  } else if (type === 'MemberExpression') {
179
180
  const { object } = element.expression.object;
180
181
 
@@ -187,7 +188,7 @@ function parseJsxElement(element, moduleContents = '') {
187
188
  // const { todos } = this;
188
189
  // ....
189
190
  // You have {todos.length} Todos left to complete
190
- string += `\$\{${element.expression.object.name}.${element.expression.property.name}\}`;
191
+ string += `$\{${element.expression.object.name}.${element.expression.property.name}}`;
191
192
  }
192
193
  }
193
194
  }
@@ -251,11 +252,13 @@ export function parseJsx(moduleURL) {
251
252
 
252
253
  walk.simple(tree, {
253
254
  ClassDeclaration(node) {
255
+ // @ts-ignore
254
256
  if (node.superClass.name === 'HTMLElement') {
255
257
  const hasShadowRoot = moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0;
256
258
 
257
259
  for (const n1 of node.body.body) {
258
260
  if (n1.type === 'MethodDefinition') {
261
+ // @ts-ignore
259
262
  const nodeName = n1.key.name;
260
263
  if (nodeName === 'render') {
261
264
  for (const n2 in n1.value.body.body) {
@@ -266,6 +269,7 @@ export function parseJsx(moduleURL) {
266
269
  ...observedAttributes,
267
270
  ...findThisReferences('render', n)
268
271
  ];
272
+ // @ts-ignore
269
273
  } else if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') {
270
274
  const html = parseJsxElement(n.argument, moduleContents);
271
275
  const elementTree = getParse(html)(html);
@@ -297,6 +301,7 @@ export function parseJsx(moduleURL) {
297
301
  sourceType: 'module'
298
302
  });
299
303
 
304
+ // @ts-ignore
300
305
  n1.value.body.body[n2] = transformed;
301
306
  }
302
307
  }
@@ -309,7 +314,9 @@ export function parseJsx(moduleURL) {
309
314
  const { declaration } = node;
310
315
 
311
316
  if (declaration && declaration.type === 'VariableDeclaration' && declaration.kind === 'const' && declaration.declarations.length === 1) {
317
+ // @ts-ignore
312
318
  if (declaration.declarations[0].id.name === 'inferredObservability') {
319
+ // @ts-ignore
313
320
  inferredObservability = Boolean(node.declaration.declarations[0].init.raw);
314
321
  }
315
322
  }
@@ -317,6 +324,7 @@ export function parseJsx(moduleURL) {
317
324
  }, {
318
325
  // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
319
326
  ...walk.base,
327
+ // @ts-ignore
320
328
  JSXElement: () => {}
321
329
  });
322
330
 
@@ -325,7 +333,9 @@ export function parseJsx(moduleURL) {
325
333
  let insertPoint;
326
334
  for (const line of tree.body) {
327
335
  // test for class MyComponent vs export default class MyComponent
336
+ // @ts-ignore
328
337
  if (line.type === 'ClassDeclaration' || (line.declaration && line.declaration.type) === 'ClassDeclaration') {
338
+ // @ts-ignore
329
339
  insertPoint = line.declaration.body.start + 1;
330
340
  }
331
341
  }
@@ -333,7 +343,6 @@ export function parseJsx(moduleURL) {
333
343
  let newModuleContents = generate(tree);
334
344
 
335
345
  // TODO better way to determine value type?
336
- /* eslint-disable indent */
337
346
  newModuleContents = `${newModuleContents.slice(0, insertPoint)}
338
347
  static get observedAttributes() {
339
348
  return [${[...observedAttributes].map(attr => `'${attr}'`).join(',')}]
@@ -366,7 +375,6 @@ export function parseJsx(moduleURL) {
366
375
 
367
376
  ${newModuleContents.slice(insertPoint)}
368
377
  `;
369
- /* eslint-enable indent */
370
378
 
371
379
  tree = acorn.Parser.extend(jsx()).parse(newModuleContents, {
372
380
  ecmaVersion: 'latest',
@@ -0,0 +1,3 @@
1
+ import { register } from 'node:module';
2
+
3
+ register('./jsx-loader.js', import.meta.url);