wc-compiler 0.18.0 → 0.19.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
@@ -8,63 +8,68 @@
8
8
  [![NodeJS compatibility](https://img.shields.io/node/v/wc-compiler.svg)](https://nodejs.org/en/about/previous-releases")
9
9
  [![Discord Chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://www.greenwoodjs.dev/discord/)
10
10
 
11
- > _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢
11
+ > _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢
12
12
 
13
13
  ## How It Works
14
14
 
15
15
  1. Write a Web Component
16
- ```js
17
- const template = document.createElement('template');
18
-
19
- template.innerHTML = `
20
- <style>
21
- .footer {
22
- color: white;
23
- background-color: #192a27;
24
- }
25
- </style>
26
-
27
- <footer class="footer">
28
- <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
29
- </footer>
30
- `;
31
-
32
- class Footer extends HTMLElement {
33
- connectedCallback() {
34
- if (!this.shadowRoot) {
35
- this.attachShadow({ mode: 'open' });
36
- this.shadowRoot.appendChild(template.content.cloneNode(true));
37
- }
38
- }
39
- }
40
-
41
- export default Footer;
42
-
43
- customElements.define('wcc-footer', Footer);
44
- ```
16
+
17
+ ```js
18
+ const template = document.createElement('template');
19
+
20
+ template.innerHTML = `
21
+ <style>
22
+ .footer {
23
+ color: white;
24
+ background-color: #192a27;
25
+ }
26
+ </style>
27
+
28
+ <footer class="footer">
29
+ <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
30
+ </footer>
31
+ `;
32
+
33
+ class Footer extends HTMLElement {
34
+ connectedCallback() {
35
+ if (!this.shadowRoot) {
36
+ this.attachShadow({ mode: 'open' });
37
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
38
+ }
39
+ }
40
+ }
41
+
42
+ export default Footer;
43
+
44
+ customElements.define('wcc-footer', Footer);
45
+ ```
46
+
45
47
  1. Run it through the compiler
46
- ```js
47
- import { renderToString } from 'wc-compiler';
48
48
 
49
- const { html } = await renderToString(new URL('./path/to/component.js', import.meta.url));
50
- ```
49
+ ```js
50
+ import { renderToString } from 'wc-compiler';
51
+
52
+ const { html } = await renderToString(new URL('./path/to/component.js', import.meta.url));
53
+ ```
54
+
51
55
  1. Get HTML!
52
- ```html
53
- <wcc-footer>
54
- <template shadowrootmode="open">
55
- <style>
56
- .footer {
57
- color: white;
58
- background-color: #192a27;
59
- }
60
- </style>
61
-
62
- <footer class="footer">
63
- <h4>My Blog &copy; 2022</h4>
64
- </footer>
65
- </template>
66
- </wcc-footer>
67
- ```
56
+
57
+ ```html
58
+ <wcc-footer>
59
+ <template shadowrootmode="open">
60
+ <style>
61
+ .footer {
62
+ color: white;
63
+ background-color: #192a27;
64
+ }
65
+ </style>
66
+
67
+ <footer class="footer">
68
+ <h4>My Blog &copy; 2022</h4>
69
+ </footer>
70
+ </template>
71
+ </wcc-footer>
72
+ ```
68
73
 
69
74
  ## Installation
70
75
 
@@ -80,6 +85,6 @@ See our [website](https://merry-caramel-524e61.netlify.app/) for API docs and ex
80
85
 
81
86
  ## Motivation
82
87
 
83
- **WCC** is not a static site generator, framework or bundler. It is designed with the intent of being able to produce raw HTML from standards compliant Web Components and easily integrated _into_ a site generator or framework, like [**Greenwood**](https://www.greenwoodjs.dev). The Project Evergreen team also maintains similar integrations for [**Eleventy**](https://github.com/ProjectEvergreen/eleventy-plugin-wcc/) and [Astro](https://github.com/ProjectEvergreen/astro-wcc).
88
+ **WCC** is not a static site generator, framework or bundler. It is designed with the intent of being able to produce raw HTML from standards compliant Web Components and easily integrated _into_ a site generator or framework, like [**Greenwood**](https://www.greenwoodjs.dev). The Project Evergreen team also maintains similar integrations for [**Eleventy**](https://github.com/ProjectEvergreen/eleventy-plugin-wcc/) and [**Astro**](https://github.com/ProjectEvergreen/astro-wcc).
84
89
 
85
- In addition, **WCC** hopes to provide a surface area to explore patterns around [streaming](https://github.com/ProjectEvergreen/wcc/issues/5), [serverless and edge rendering](https://github.com/thescientist13/web-components-at-the-edge), and as acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg)'s discussions around community protocols, like [hydration](https://github.com/ProjectEvergreen/wcc/issues/3).
90
+ In addition, **WCC** hopes to provide a surface area to explore patterns around [streaming](https://github.com/ProjectEvergreen/wcc/issues/5), [serverless and edge rendering](https://github.com/thescientist13/web-components-at-the-edge), and acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg)'s discussions around community protocols, like [hydration](https://github.com/ProjectEvergreen/wcc/issues/3).
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "wc-compiler",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Experimental native Web Components compiler.",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/ProjectEvergreen/wcc.git"
7
+ "url": "git+https://github.com/ProjectEvergreen/wcc.git"
8
8
  },
9
9
  "type": "module",
10
10
  "main": "src/wcc.js",
11
11
  "types": "./src/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
- "import": "./src/wcc.js",
15
- "types": "./src/index.d.ts"
14
+ "types": "./src/index.d.ts",
15
+ "import": "./src/wcc.js"
16
16
  },
17
17
  "./register": "./src/register.js",
18
18
  "./src/jsx-loader.js": "./src/jsx-loader.js",
@@ -37,8 +37,12 @@
37
37
  },
38
38
  "scripts": {
39
39
  "clean": "rimraf ./dist",
40
- "lint": "eslint",
40
+ "lint": "npm run lint:js && npm run lint:ls",
41
+ "lint:js": "eslint",
42
+ "lint:ls": "ls-lint",
41
43
  "lint:types": "tsc",
44
+ "format": "prettier . --write",
45
+ "format:check": "prettier . --check",
42
46
  "docs:dev": "concurrently \"nodemon --watch src --watch docs -e js,md,css,html,jsx,ts,tsx ./build.js\" \"http-server ./dist --open\"",
43
47
  "docs:build": "node ./build.js",
44
48
  "docs:serve": "npm run clean && npm run docs:build && http-server ./dist --open",
@@ -47,7 +51,8 @@
47
51
  "test": "mocha --exclude \"./test/cases/jsx*/**\" --exclude \"./test/cases/ts*/**\" --exclude \"./test/cases/custom-extension/**\" \"./test/**/**/*.spec.js\"",
48
52
  "test:jsx": "c8 node --import ./test-register.js --experimental-strip-types ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"",
49
53
  "test:tdd": "npm run test -- --watch",
50
- "test:tdd:jsx": "npm run test:jsx -- --watch"
54
+ "test:tdd:jsx": "npm run test:jsx -- --watch",
55
+ "prepare": "husky"
51
56
  },
52
57
  "dependencies": {
53
58
  "@projectevergreen/acorn-jsx-esm": "~0.1.0",
@@ -58,25 +63,26 @@
58
63
  "sucrase": "^3.35.0"
59
64
  },
60
65
  "devDependencies": {
61
- "@babel/core": "^7.24.4",
62
- "@babel/eslint-parser": "^7.25.7",
63
- "@babel/plugin-syntax-import-assertions": "^7.25.7",
64
- "@eslint/js": "^9.11.1",
65
- "@ls-lint/ls-lint": "^1.10.0",
66
+ "@eslint/js": "^9.39.1",
67
+ "@ls-lint/ls-lint": "^2.3.1",
66
68
  "@mapbox/rehype-prism": "^0.8.0",
67
69
  "@types/mocha": "^10.0.10",
68
70
  "@types/node": "^22.13.4",
69
71
  "c8": "^7.11.2",
70
72
  "chai": "^4.3.6",
71
73
  "concurrently": "^7.1.0",
72
- "eslint": "^9.11.1",
73
- "eslint-plugin-no-only-tests": "^2.6.0",
74
+ "eslint": "^9.39.1",
75
+ "eslint-config-prettier": "^10.1.8",
76
+ "eslint-plugin-no-only-tests": "^3.3.0",
74
77
  "globals": "^15.10.0",
75
78
  "http-server": "^14.1.0",
79
+ "husky": "^9.1.7",
76
80
  "jsdom": "^19.0.0",
81
+ "lint-staged": "^16.2.6",
77
82
  "livereload": "^0.9.3",
78
83
  "mocha": "^9.2.2",
79
84
  "nodemon": "^2.0.15",
85
+ "prettier": "^3.6.2",
80
86
  "prismjs": "^1.28.0",
81
87
  "rehype-autolink-headings": "^6.1.1",
82
88
  "rehype-raw": "^6.1.1",
package/src/dom-shim.js CHANGED
@@ -46,18 +46,20 @@ function getParse5ElementDefaults(element, tagName) {
46
46
  nodeName: tagName,
47
47
  tagName: tagName,
48
48
  namespaceURI: 'http://www.w3.org/1999/xhtml',
49
- ...(tagName === 'template' ? { content: { nodeName: '#document-fragment', childNodes: [] } } : {})
49
+ ...(tagName === 'template'
50
+ ? { content: { nodeName: '#document-fragment', childNodes: [] } }
51
+ : {}),
50
52
  };
51
53
  }
52
54
 
53
- function noop() { }
55
+ function noop() {}
54
56
 
55
57
  // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
56
58
  class CSSStyleSheet {
57
- insertRule() { }
58
- deleteRule() { }
59
- replace() { }
60
- replaceSync() { }
59
+ insertRule() {}
60
+ deleteRule() {}
61
+ replace() {}
62
+ replaceSync() {}
61
63
  }
62
64
 
63
65
  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
@@ -132,7 +134,7 @@ class Node extends EventTarget {
132
134
  }
133
135
 
134
136
  return this.childNodes
135
- .map((child) => child.nodeName === '#text' ? child.value : child.textContent)
137
+ .map((child) => (child.nodeName === '#text' ? child.value : child.textContent))
136
138
  .join('');
137
139
  }
138
140
 
@@ -163,7 +165,9 @@ class Element extends Node {
163
165
  }
164
166
 
165
167
  getHTML({ serializableShadowRoots = false }) {
166
- return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable ? this.shadowRoot.innerHTML : '';
168
+ return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable
169
+ ? this.shadowRoot.innerHTML
170
+ : '';
167
171
  }
168
172
 
169
173
  get innerHTML() {
@@ -172,7 +176,8 @@ class Element extends Node {
172
176
  }
173
177
 
174
178
  set innerHTML(html) {
175
- (this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes;
179
+ (this.nodeName === 'template' ? this.content : this).childNodes =
180
+ getParse(html)(html).childNodes;
176
181
  }
177
182
 
178
183
  hasAttribute(name) {
@@ -198,16 +203,13 @@ class Element extends Node {
198
203
  // https://developer.mozilla.org/en-US/docs/Web/API/Document
199
204
  // EventTarget <- Node <- Document
200
205
  class Document extends Node {
201
-
202
206
  createElement(tagName) {
203
207
  switch (tagName) {
204
-
205
208
  case 'template':
206
209
  return new HTMLTemplateElement();
207
210
 
208
211
  default:
209
212
  return new HTMLElement(tagName);
210
-
211
213
  }
212
214
  }
213
215
 
@@ -223,12 +225,12 @@ class HTMLElement extends Element {
223
225
  super();
224
226
  Object.assign(this, getParse5ElementDefaults(this, tagName));
225
227
  }
226
- connectedCallback() { }
228
+ connectedCallback() {}
227
229
  }
228
230
 
229
231
  // https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
230
232
  // EventTarget <- Node <- DocumentFragment
231
- class DocumentFragment extends Node { }
233
+ class DocumentFragment extends Node {}
232
234
 
233
235
  // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
234
236
  // EventTarget <- Node <- DocumentFragment <- ShadowRoot
@@ -241,11 +243,15 @@ class ShadowRoot extends DocumentFragment {
241
243
  }
242
244
 
243
245
  get innerHTML() {
244
- return this.childNodes?.[0]?.content?.childNodes ? serialize({ childNodes: this.childNodes[0].content.childNodes }) : '';
246
+ return this.childNodes?.[0]?.content?.childNodes
247
+ ? serialize({ childNodes: this.childNodes[0].content.childNodes })
248
+ : '';
245
249
  }
246
250
 
247
251
  set innerHTML(html) {
248
- this.childNodes = getParse(html)(`<template shadowrootmode="${this.mode}">${html}</template>`).childNodes;
252
+ this.childNodes = getParse(html)(
253
+ `<template shadowrootmode="${this.mode}">${html}</template>`,
254
+ ).childNodes;
249
255
  }
250
256
  }
251
257
 
@@ -290,4 +296,4 @@ globalThis.document = globalThis.document ?? new Document();
290
296
  globalThis.customElements = globalThis.customElements ?? new CustomElementsRegistry();
291
297
  globalThis.HTMLElement = globalThis.HTMLElement ?? HTMLElement;
292
298
  globalThis.DocumentFragment = globalThis.DocumentFragment ?? DocumentFragment;
293
- globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? CSSStyleSheet;
299
+ globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? CSSStyleSheet;
package/src/index.d.ts CHANGED
@@ -2,21 +2,29 @@ export type Metadata = {
2
2
  [key: string]: {
3
3
  instanceName: string;
4
4
  moduleURL: URL;
5
- isEntry: boolean
6
- }
7
- }
5
+ isEntry: boolean;
6
+ source: string;
7
+ };
8
+ };
8
9
 
9
- export type renderToString = (elementURL: URL, wrappingEntryTag?: boolean, props?: any) => Promise<{
10
+ export type renderToString = (
11
+ elementURL: URL,
12
+ wrappingEntryTag?: boolean,
13
+ props?: any,
14
+ ) => Promise<{
10
15
  html: string;
11
- metadata: Metadata
12
- }>
16
+ metadata: Metadata;
17
+ }>;
13
18
 
14
- export type renderFromHTML = (html: string, elementURLs: URL[]) => Promise<{
19
+ export type renderFromHTML = (
20
+ html: string,
21
+ elementURLs: URL[],
22
+ ) => Promise<{
15
23
  html: string;
16
- metadata: Metadata
17
- }>
24
+ metadata: Metadata;
25
+ }>;
18
26
 
19
- declare module "wc-compiler" {
27
+ declare module 'wc-compiler' {
20
28
  export const renderToString: renderToString;
21
29
  export const renderFromHTML: renderFromHTML;
22
- }
30
+ }
package/src/jsx-loader.js CHANGED
@@ -39,8 +39,8 @@ export function getParser(moduleURL) {
39
39
  config: {
40
40
  // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
41
41
  ...walk.base,
42
- JSXElement: () => {}
43
- }
42
+ JSXElement: () => {},
43
+ },
44
44
  };
45
45
  }
46
46
 
@@ -57,7 +57,9 @@ function applyDomDepthSubstitutions(tree, currentDepth = 1, hasShadowRoot = fals
57
57
  const { value } = attrs[attr];
58
58
 
59
59
  if (value.indexOf('__this__.') >= 0) {
60
- const root = hasShadowRoot ? '.getRootNode().host' : `${'.parentElement'.repeat(currentDepth)}`;
60
+ const root = hasShadowRoot
61
+ ? '.getRootNode().host'
62
+ : `${'.parentElement'.repeat(currentDepth)}`;
61
63
 
62
64
  node.attrs[attr].value = value.replace(/__this__/g, `this${root}`);
63
65
  }
@@ -75,7 +77,7 @@ function applyDomDepthSubstitutions(tree, currentDepth = 1, hasShadowRoot = fals
75
77
  return tree;
76
78
  }
77
79
 
78
- function parseJsxElement(element, moduleContents = '') {
80
+ function parseJsxElement(element, moduleContents = '', inferredObservability = false) {
79
81
  try {
80
82
  const { type } = element;
81
83
 
@@ -122,8 +124,14 @@ function parseJsxElement(element, moduleContents = '') {
122
124
 
123
125
  if (left.object.type === 'ThisExpression') {
124
126
  if (left.property.type === 'Identifier') {
125
- // very naive (fine grained?) reactivity
126
- string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.render();"`;
127
+ if (inferredObservability) {
128
+ // very naive (fine grained?) reactivity
129
+ // string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.update(\\'${left.property.name}\\', null, __this__.${left.property.name});"`;
130
+ string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.setAttribute(\\'${left.property.name}\\', __this__.${left.property.name});"`;
131
+ } else {
132
+ // implicit reactivity using this.render
133
+ string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.render();"`;
134
+ }
127
135
  }
128
136
  }
129
137
  }
@@ -139,17 +147,29 @@ function parseJsxElement(element, moduleContents = '') {
139
147
  // xxx={allTodos.length} >
140
148
  const { value } = attribute;
141
149
  const { expression } = value;
150
+ const expressionType = expression.type;
142
151
 
143
- if (expression.type === 'Identifier') {
144
- string += ` ${name}=$\{${expression.name}}`;
152
+ switch (expressionType) {
153
+ case 'Literal':
154
+ string += ` ${name}=${expression.raw}`;
155
+ break;
156
+ case 'Identifier':
157
+ string += ` ${name}=$\{${expression.name}}`;
158
+ break;
159
+ case 'MemberExpression':
160
+ if (expression.object.type === 'Identifier') {
161
+ if (expression.property.type === 'Identifier') {
162
+ string += ` ${name}=$\{${expression.object.name}.${expression.property.name}}`;
163
+ }
164
+ }
165
+ break;
166
+ default:
167
+ break;
145
168
  }
146
169
 
147
- if (expression.type === 'MemberExpression') {
148
- if (expression.object.type === 'Identifier') {
149
- if (expression.property.type === 'Identifier') {
150
- string += ` ${name}=$\{${expression.object.name}.${expression.property.name}}`;
151
- }
152
- }
170
+ // only apply this when dealing with `this` references
171
+ if (inferredObservability) {
172
+ string += ` data-wcc-${expression.name}="${name}" data-wcc-ins="attr"`;
153
173
  }
154
174
  }
155
175
  } else {
@@ -159,10 +179,12 @@ function parseJsxElement(element, moduleContents = '') {
159
179
  }
160
180
  }
161
181
 
162
- string += openingElement.selfClosing ? '/>' : '>';
182
+ string += openingElement.selfClosing ? ' />' : '>';
163
183
 
164
184
  if (element.children.length > 0) {
165
- element.children.forEach(child => parseJsxElement(child, moduleContents));
185
+ element.children.forEach((child) =>
186
+ parseJsxElement(child, moduleContents, inferredObservability),
187
+ );
166
188
  }
167
189
 
168
190
  string += `</${tagName}>`;
@@ -177,6 +199,13 @@ function parseJsxElement(element, moduleContents = '') {
177
199
 
178
200
  if (type === 'Identifier') {
179
201
  // You have {count} TODOs left to complete
202
+ if (inferredObservability) {
203
+ const { name } = element.expression;
204
+
205
+ string = `${string.slice(0, string.lastIndexOf('>'))} data-wcc-${name}="\${this.${name}}" data-wcc-ins="text">`;
206
+ }
207
+ // TODO be able to remove this extra data attribute
208
+ // string = `${string.slice(0, string.lastIndexOf('>'))} data-wcc-${name} data-wcc-ins="text">`;
180
209
  string += `$\{${element.expression.name}}`;
181
210
  } else if (type === 'MemberExpression') {
182
211
  const { object } = element.expression.object;
@@ -207,26 +236,33 @@ function findThisReferences(context, statement) {
207
236
  const references = [];
208
237
  const isRenderFunctionContext = context === 'render';
209
238
  const { expression, type } = statement;
210
- const isConstructorThisAssignment = context === 'constructor'
211
- && type === 'ExpressionStatement'
212
- && expression.type === 'AssignmentExpression'
213
- && expression.left.object.type === 'ThisExpression';
239
+ const isConstructorThisAssignment =
240
+ context === 'constructor' &&
241
+ type === 'ExpressionStatement' &&
242
+ expression.type === 'AssignmentExpression' &&
243
+ expression.left.object.type === 'ThisExpression';
214
244
 
215
245
  if (isConstructorThisAssignment) {
216
246
  // this.name = 'something'; // constructor
217
247
  references.push(expression.left.property.name);
218
248
  } else if (isRenderFunctionContext && type === 'VariableDeclaration') {
219
- statement.declarations.forEach(declaration => {
249
+ statement.declarations.forEach((declaration) => {
220
250
  const { init, id } = declaration;
221
251
 
222
252
  if (init.object && init.object.type === 'ThisExpression') {
223
253
  // const { description } = this.todo;
224
254
  references.push(init.property.name);
225
255
  } else if (init.type === 'ThisExpression' && id && id.properties) {
226
- // const { description } = this.todo;
256
+ // const { id, description } = this;
227
257
  id.properties.forEach((property) => {
228
258
  references.push(property.key.name);
229
259
  });
260
+ } else {
261
+ // TODO we are just blindly tracking anything here.
262
+ // everything should ideally be mapped to actual this references, to create a strong chain of direct reactivity
263
+ // instead of tracking any declaration as a derived tracking attr
264
+ // for convenience here, we push the entire declaration here, instead of the name like for direct this references (see above)
265
+ references.push(declaration);
230
266
  }
231
267
  });
232
268
  }
@@ -238,7 +274,7 @@ export function parseJsx(moduleURL) {
238
274
  const moduleContents = fs.readFileSync(moduleURL, 'utf-8');
239
275
  const result = transform(moduleContents, {
240
276
  transforms: ['typescript', 'jsx'],
241
- jsxRuntime: 'preserve'
277
+ jsxRuntime: 'preserve',
242
278
  });
243
279
  // would be nice if we could do this instead, so we could know ahead of time
244
280
  // const { inferredObservability } = await import(moduleURL);
@@ -248,45 +284,77 @@ export function parseJsx(moduleURL) {
248
284
  let observedAttributes = [];
249
285
  let tree = acorn.Parser.extend(jsx()).parse(result.code, {
250
286
  ecmaVersion: 'latest',
251
- sourceType: 'module'
287
+ sourceType: 'module',
252
288
  });
253
289
  string = '';
254
290
 
255
- walk.simple(tree, {
256
- ClassDeclaration(node) {
291
+ // TODO: would be nice to do this one pass, but first we need to know if `inferredObservability` is set first
292
+ walk.simple(
293
+ tree,
294
+ {
295
+ ExportNamedDeclaration(node) {
296
+ const { declaration } = node;
297
+
298
+ if (
299
+ declaration &&
300
+ declaration.type === 'VariableDeclaration' &&
301
+ declaration.kind === 'const' &&
302
+ declaration.declarations.length === 1
303
+ ) {
304
+ // @ts-ignore
305
+ if (declaration.declarations[0].id.name === 'inferredObservability') {
306
+ // @ts-ignore
307
+ inferredObservability = Boolean(node.declaration.declarations[0].init.raw);
308
+ }
309
+ }
310
+ },
311
+ },
312
+ {
313
+ // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
314
+ ...walk.base,
257
315
  // @ts-ignore
258
- if (node.superClass.name === 'HTMLElement') {
259
- const hasShadowRoot = moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0;
316
+ JSXElement: () => {},
317
+ },
318
+ );
260
319
 
261
- for (const n1 of node.body.body) {
262
- if (n1.type === 'MethodDefinition') {
263
- // @ts-ignore
264
- const nodeName = n1.key.name;
265
- if (nodeName === 'render') {
266
- for (const n2 in n1.value.body.body) {
267
- const n = n1.value.body.body[n2];
268
-
269
- if (n.type === 'VariableDeclaration') {
270
- observedAttributes = [
271
- ...observedAttributes,
272
- ...findThisReferences('render', n)
273
- ];
274
- // @ts-ignore
275
- } else if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') {
276
- const html = parseJsxElement(n.argument, moduleContents);
277
- const elementTree = getParse(html)(html);
278
- const elementRoot = hasShadowRoot ? 'this.shadowRoot' : 'this';
279
-
280
- applyDomDepthSubstitutions(elementTree, undefined, hasShadowRoot);
281
-
282
- const serializedHtml = serialize(elementTree);
283
- // we have to Shadow DOM use cases here
284
- // 1. No shadowRoot, so we attachShadow and append the template
285
- // 2. If there is root from the attachShadow signal, so we just need to inject innerHTML, say in an htmx
286
- // could / should we do something else instead of .innerHTML
287
- // https://github.com/ProjectEvergreen/wcc/issues/138
288
- const renderHandler = hasShadowRoot
289
- ? `
320
+ walk.simple(
321
+ tree,
322
+ {
323
+ ClassDeclaration(node) {
324
+ // @ts-ignore
325
+ if (node.superClass.name === 'HTMLElement') {
326
+ const hasShadowRoot =
327
+ moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0;
328
+
329
+ for (const n1 of node.body.body) {
330
+ if (n1.type === 'MethodDefinition') {
331
+ // @ts-ignore
332
+ const nodeName = n1.key.name;
333
+ if (nodeName === 'render') {
334
+ for (const n2 in n1.value.body.body) {
335
+ const n = n1.value.body.body[n2];
336
+
337
+ if (n.type === 'VariableDeclaration') {
338
+ observedAttributes = [
339
+ ...observedAttributes,
340
+ ...findThisReferences('render', n),
341
+ ];
342
+ // @ts-ignore
343
+ } else if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') {
344
+ const html = parseJsxElement(n.argument, moduleContents, inferredObservability);
345
+ const elementTree = getParse(html)(html);
346
+ const elementRoot = hasShadowRoot ? 'this.shadowRoot' : 'this';
347
+
348
+ applyDomDepthSubstitutions(elementTree, undefined, hasShadowRoot);
349
+
350
+ const serializedHtml = serialize(elementTree);
351
+ // we have to Shadow DOM use cases here
352
+ // 1. No shadowRoot, so we attachShadow and append the template
353
+ // 2. If there is root from the attachShadow signal, so we just need to inject innerHTML, say in an htmx
354
+ // could / should we do something else instead of .innerHTML
355
+ // https://github.com/ProjectEvergreen/wcc/issues/138
356
+ const renderHandler = hasShadowRoot
357
+ ? `
290
358
  const template = document.createElement('template');
291
359
  template.innerHTML = \`${serializedHtml}\`;
292
360
 
@@ -297,57 +365,75 @@ export function parseJsx(moduleURL) {
297
365
  this.shadowRoot.innerHTML = template.innerHTML;
298
366
  }
299
367
  `
300
- : `${elementRoot}.innerHTML = \`${serializedHtml}\`;`;
301
- const transformed = acorn.parse(renderHandler, {
302
- ecmaVersion: 'latest',
303
- sourceType: 'module'
304
- });
305
-
306
- // @ts-ignore
307
- n1.value.body.body[n2] = transformed;
368
+ : `${elementRoot}.innerHTML = \`${serializedHtml}\`;`;
369
+ const transformed = acorn.parse(renderHandler, {
370
+ ecmaVersion: 'latest',
371
+ sourceType: 'module',
372
+ });
373
+
374
+ // @ts-ignore
375
+ n1.value.body.body[n2] = transformed;
376
+ }
308
377
  }
309
378
  }
310
379
  }
311
380
  }
312
381
  }
313
- }
382
+ },
314
383
  },
315
- ExportNamedDeclaration(node) {
316
- const { declaration } = node;
317
-
318
- if (declaration && declaration.type === 'VariableDeclaration' && declaration.kind === 'const' && declaration.declarations.length === 1) {
319
- // @ts-ignore
320
- if (declaration.declarations[0].id.name === 'inferredObservability') {
321
- // @ts-ignore
322
- inferredObservability = Boolean(node.declaration.declarations[0].init.raw);
323
- }
324
- }
325
- }
326
- }, {
327
- // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
328
- ...walk.base,
329
- // @ts-ignore
330
- JSXElement: () => {}
331
- });
384
+ {
385
+ // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171
386
+ ...walk.base,
387
+ // @ts-ignore
388
+ JSXElement: () => {},
389
+ },
390
+ );
332
391
 
333
- // TODO - signals: use constructor, render, HTML attributes? some, none, or all?
334
392
  if (inferredObservability && observedAttributes.length > 0 && !hasOwnObservedAttributes) {
335
393
  let insertPoint;
336
394
  for (const line of tree.body) {
337
- // test for class MyComponent vs export default class MyComponent
395
+ // TODO: test for class MyComponent vs export default class MyComponent
338
396
  // @ts-ignore
339
- if (line.type === 'ClassDeclaration' || (line.declaration && line.declaration.type) === 'ClassDeclaration') {
397
+ if (
398
+ line.type === 'ClassDeclaration' ||
399
+ (line.declaration && line.declaration.type) === 'ClassDeclaration'
400
+ ) {
340
401
  // @ts-ignore
341
402
  insertPoint = line.declaration.body.start + 1;
342
403
  }
343
404
  }
344
405
 
345
406
  let newModuleContents = generate(tree);
346
-
347
- // TODO better way to determine value type?
407
+ const trackingAttrs = observedAttributes.filter((attr) => typeof attr === 'string');
408
+ // TODO ideally derivedAttrs would explicitly reference trackingAttrs
409
+ // and if there are no derivedAttrs, do not include the derivedGetters / derivedSetters code in the compiled output
410
+ const derivedAttrs = observedAttributes.filter((attr) => typeof attr !== 'string');
411
+ const derivedGetters = derivedAttrs
412
+ .map((attr) => {
413
+ return `
414
+ get_${attr.id.name}(${trackingAttrs.join(',')}) {
415
+ return ${moduleContents.slice(attr.init.start, attr.init.end)}
416
+ }
417
+ `;
418
+ })
419
+ .join('\n');
420
+ const derivedSetters = derivedAttrs
421
+ .map((attr) => {
422
+ const name = attr.id.name;
423
+
424
+ return `
425
+ const old_${name} = this.get_${name}(oldValue);
426
+ const new_${name} = this.get_${name}(newValue);
427
+ this.update('${name}', old_${name}, new_${name});
428
+ `;
429
+ })
430
+ .join('\n');
431
+
432
+ // TODO: better way to determine value type, e,g. array, int, object, etc?
433
+ // TODO: better way to test for shadowRoot presence when running querySelectorAll
348
434
  newModuleContents = `${newModuleContents.slice(0, insertPoint)}
349
435
  static get observedAttributes() {
350
- return [${[...observedAttributes].map(attr => `'${attr}'`).join(',')}]
436
+ return [${[...trackingAttrs].map((attr) => `'${attr}'`).join()}]
351
437
  }
352
438
 
353
439
  attributeChangedCallback(name, oldValue, newValue) {
@@ -362,25 +448,53 @@ export function parseJsx(moduleURL) {
362
448
  }
363
449
  if (newValue !== oldValue) {
364
450
  switch(name) {
365
- ${observedAttributes.map((attr) => {
366
- return `
367
- case '${attr}':
368
- this.${attr} = getValue(newValue);
369
- break;
370
- `;
371
- }).join('\n')}
451
+ ${trackingAttrs
452
+ .map((attr) => {
453
+ return `
454
+ case '${attr}':
455
+ this.${attr} = getValue(newValue);
456
+ break;
457
+ `;
458
+ })
459
+ .join('\n')}
372
460
  }
461
+ this.update(name, oldValue, newValue);
462
+ }
463
+ }
373
464
 
374
- this.render();
465
+ update(name, oldValue, newValue) {
466
+ const attr = \`data-wcc-\${name}\`;
467
+ const selector = \`[\${attr}]\`;
468
+
469
+ (this?.shadowRoot || this).querySelectorAll(selector).forEach((el) => {
470
+ // handle empty strings as a value for the purposes of attribute change detection
471
+ const needle = oldValue === '' ? '' : oldValue ?? el.getAttribute(attr);
472
+
473
+ switch(el.getAttribute('data-wcc-ins')) {
474
+ case 'text':
475
+ el.textContent = el.textContent.replace(needle, newValue);
476
+ break;
477
+ case 'attr':
478
+ if (el.hasAttribute(el.getAttribute(attr))) {
479
+ el.setAttribute(el.getAttribute(attr), newValue);
480
+ }
481
+ break;
482
+ }
483
+ })
484
+
485
+ if ([${[...trackingAttrs].map((attr) => `'${attr}'`).join()}].includes(name)) {
486
+ ${derivedSetters}
375
487
  }
376
488
  }
377
489
 
490
+ ${derivedGetters}
491
+
378
492
  ${newModuleContents.slice(insertPoint)}
379
493
  `;
380
494
 
381
495
  tree = acorn.Parser.extend(jsx()).parse(newModuleContents, {
382
496
  ecmaVersion: 'latest',
383
- sourceType: 'module'
497
+ sourceType: 'module',
384
498
  });
385
499
  }
386
500
 
@@ -395,7 +509,7 @@ export function resolve(specifier, context, defaultResolve) {
395
509
  if (jsxRegex.test(specifier) || tsxRegex.test(specifier)) {
396
510
  return {
397
511
  url: new URL(specifier, parentURL).href,
398
- shortCircuit: true
512
+ shortCircuit: true,
399
513
  };
400
514
  }
401
515
 
@@ -409,9 +523,9 @@ export async function load(url, context, defaultLoad) {
409
523
  return {
410
524
  format: 'module',
411
525
  source: generate(jsFromJsx),
412
- shortCircuit: true
526
+ shortCircuit: true,
413
527
  };
414
528
  }
415
529
 
416
530
  return defaultLoad(url, context, defaultLoad);
417
- }
531
+ }
@@ -1 +1 @@
1
- import './jsx.d.ts';
1
+ import './jsx.d.ts';
package/src/jsx.d.ts CHANGED
@@ -17,4 +17,4 @@ type IntrinsicElementsFromDom = {
17
17
  // declare the global JSX namespace with your generated intrinsic elements.
18
18
  declare namespace JSX {
19
19
  interface IntrinsicElements extends IntrinsicElementsFromDom {}
20
- }
20
+ }
package/src/register.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { register } from 'node:module';
2
2
 
3
- register('./jsx-loader.js', import.meta.url);
3
+ register('./jsx-loader.js', import.meta.url);
package/src/wcc.js CHANGED
@@ -12,9 +12,14 @@ import fs from 'fs';
12
12
  function isCustomElementDefinitionNode(node) {
13
13
  const { expression } = node;
14
14
 
15
- return expression.type === 'CallExpression' && expression.callee && expression.callee.object
16
- && expression.callee.property && expression.callee.object.name === 'customElements'
17
- && expression.callee.property.name === 'define';
15
+ return (
16
+ expression.type === 'CallExpression' &&
17
+ expression.callee &&
18
+ expression.callee.object &&
19
+ expression.callee.property &&
20
+ expression.callee.object.name === 'customElements' &&
21
+ expression.callee.property.name === 'define'
22
+ );
18
23
  }
19
24
 
20
25
  async function renderComponentRoots(tree, definitions) {
@@ -24,7 +29,12 @@ async function renderComponentRoots(tree, definitions) {
24
29
 
25
30
  if (definitions[tagName]) {
26
31
  const { moduleURL } = definitions[tagName];
27
- const elementInstance = await initializeCustomElement(moduleURL, tagName, node, definitions);
32
+ const elementInstance = await initializeCustomElement(
33
+ moduleURL,
34
+ tagName,
35
+ node,
36
+ definitions,
37
+ );
28
38
 
29
39
  if (elementInstance) {
30
40
  const hasShadow = elementInstance.shadowRoot;
@@ -33,10 +43,14 @@ async function renderComponentRoots(tree, definitions) {
33
43
  ? [...elementInstance.shadowRoot.childNodes, ...node.childNodes]
34
44
  : elementInstance.childNodes;
35
45
  } else {
36
- console.warn(`WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`);
46
+ console.warn(
47
+ `WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`,
48
+ );
37
49
  }
38
50
  } else {
39
- console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it.`);
51
+ console.warn(
52
+ `WARNING: customElement <${tagName}> is not defined. You may not have imported it.`,
53
+ );
40
54
  }
41
55
 
42
56
  attrs.forEach((attr) => {
@@ -44,7 +58,6 @@ async function renderComponentRoots(tree, definitions) {
44
58
  definitions[tagName].hydrate = attr.value;
45
59
  }
46
60
  });
47
-
48
61
  }
49
62
 
50
63
  if (node.childNodes && node.childNodes.length > 0) {
@@ -71,53 +84,60 @@ function registerDependencies(moduleURL, definitions, depth = 0) {
71
84
  jsxRuntime: 'automatic',
72
85
  production: true,
73
86
  });
74
- const nextDepth = depth += 1;
87
+ const nextDepth = (depth += 1);
75
88
  const customParser = getParser(moduleURL);
76
89
  const parser = customParser ? customParser.parser : acorn.Parser;
77
- const config = customParser ? customParser.config : {
78
- ...walk.base
79
- };
80
-
81
- walk.simple(parser.parse(result.code, {
82
- ecmaVersion: 'latest',
83
- sourceType: 'module'
84
- }), {
85
- ImportDeclaration(node) {
86
- const specifier = node.source.value;
87
-
88
- if (typeof specifier === 'string') {
89
- const isBareSpecifier = specifier.indexOf('.') !== 0 && specifier.indexOf('/') !== 0;
90
- const extension = typeof specifier === "string" ? specifier.split('.').pop() : "";
91
-
92
- // would like to decouple .jsx from the core, ideally
93
- // https://github.com/ProjectEvergreen/wcc/issues/122
94
- if (!isBareSpecifier && ['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
95
- const dependencyModuleURL = new URL(specifier, moduleURL);
96
-
97
- registerDependencies(dependencyModuleURL, definitions, nextDepth);
90
+ const config = customParser
91
+ ? customParser.config
92
+ : {
93
+ ...walk.base,
94
+ };
95
+
96
+ walk.simple(
97
+ parser.parse(result.code, {
98
+ ecmaVersion: 'latest',
99
+ sourceType: 'module',
100
+ }),
101
+ {
102
+ ImportDeclaration(node) {
103
+ const specifier = node.source.value;
104
+
105
+ if (typeof specifier === 'string') {
106
+ const isBareSpecifier = specifier.indexOf('.') !== 0 && specifier.indexOf('/') !== 0;
107
+ const extension = typeof specifier === 'string' ? specifier.split('.').pop() : '';
108
+
109
+ // would like to decouple .jsx from the core, ideally
110
+ // https://github.com/ProjectEvergreen/wcc/issues/122
111
+ if (!isBareSpecifier && ['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
112
+ const dependencyModuleURL = new URL(specifier, moduleURL);
113
+
114
+ registerDependencies(dependencyModuleURL, definitions, nextDepth);
115
+ }
98
116
  }
99
- }
117
+ },
118
+ ExpressionStatement(node) {
119
+ if (isCustomElementDefinitionNode(node)) {
120
+ // @ts-ignore
121
+ const { arguments: args } = node.expression;
122
+ const tagName =
123
+ args[0].type === 'Literal'
124
+ ? args[0].value // single and double quotes
125
+ : args[0].quasis[0].value.raw; // template literal
126
+ const tree = parseJsx(moduleURL);
127
+ const isEntry = nextDepth - 1 === 1;
128
+
129
+ definitions[tagName] = {
130
+ instanceName: args[1].name,
131
+ moduleURL,
132
+ source: generate(tree),
133
+ url: moduleURL,
134
+ isEntry,
135
+ };
136
+ }
137
+ },
100
138
  },
101
- ExpressionStatement(node) {
102
- if (isCustomElementDefinitionNode(node)) {
103
- // @ts-ignore
104
- const { arguments: args } = node.expression;
105
- const tagName = args[0].type === 'Literal'
106
- ? args[0].value // single and double quotes
107
- : args[0].quasis[0].value.raw; // template literal
108
- const tree = parseJsx(moduleURL);
109
- const isEntry = nextDepth - 1 === 1;
110
-
111
- definitions[tagName] = {
112
- instanceName: args[1].name,
113
- moduleURL,
114
- source: generate(tree),
115
- url: moduleURL,
116
- isEntry
117
- };
118
- }
119
- }
120
- }, config);
139
+ config,
140
+ );
121
141
  }
122
142
 
123
143
  async function getTagName(moduleURL) {
@@ -129,28 +149,40 @@ async function getTagName(moduleURL) {
129
149
  });
130
150
  const customParser = getParser(moduleURL);
131
151
  const parser = customParser ? customParser.parser : acorn.Parser;
132
- const config = customParser ? customParser.config : {
133
- ...walk.base
134
- };
152
+ const config = customParser
153
+ ? customParser.config
154
+ : {
155
+ ...walk.base,
156
+ };
135
157
  let tagName;
136
158
 
137
- walk.simple(parser.parse(result.code, {
138
- ecmaVersion: 'latest',
139
- sourceType: 'module'
140
- }), {
141
- ExpressionStatement(node) {
142
- if (isCustomElementDefinitionNode(node)) {
143
- // @ts-ignore
144
- tagName = node.expression.arguments[0].value;
145
- }
146
- }
147
- }, config);
159
+ walk.simple(
160
+ parser.parse(result.code, {
161
+ ecmaVersion: 'latest',
162
+ sourceType: 'module',
163
+ }),
164
+ {
165
+ ExpressionStatement(node) {
166
+ if (isCustomElementDefinitionNode(node)) {
167
+ // @ts-ignore
168
+ tagName = node.expression.arguments[0].value;
169
+ }
170
+ },
171
+ },
172
+ config,
173
+ );
148
174
 
149
175
  return tagName;
150
176
  }
151
177
 
152
- async function initializeCustomElement(elementURL, tagName, node = {}, definitions = {}, isEntry, props = {}) {
153
-
178
+ async function initializeCustomElement(
179
+ elementURL,
180
+ tagName,
181
+ node = {},
182
+ definitions = {},
183
+ isEntry,
184
+ props = {},
185
+ ) {
154
186
  if (!tagName) {
155
187
  const depth = isEntry ? 1 : 0;
156
188
  registerDependencies(elementURL, definitions, depth);
@@ -173,9 +205,16 @@ async function initializeCustomElement(elementURL, tagName, node = {}, definitio
173
205
 
174
206
  async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
175
207
  const definitions = {};
176
- const elementTagName = wrappingEntryTag && await getTagName(elementURL);
208
+ const elementTagName = wrappingEntryTag && (await getTagName(elementURL));
177
209
  const isEntry = !!elementTagName;
178
- const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry, props);
210
+ const elementInstance = await initializeCustomElement(
211
+ elementURL,
212
+ undefined,
213
+ undefined,
214
+ definitions,
215
+ isEntry,
216
+ props,
217
+ );
179
218
 
180
219
  let html;
181
220
 
@@ -186,28 +225,29 @@ async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
186
225
 
187
226
  await renderComponentRoots(
188
227
  elementInstance.shadowRoot
189
- ?
190
- {
191
- nodeName: '#document-fragment',
192
- childNodes: [elementInstance]
193
- }
228
+ ? {
229
+ nodeName: '#document-fragment',
230
+ childNodes: [elementInstance],
231
+ }
194
232
  : elementInstance,
195
- definitions
233
+ definitions,
196
234
  );
197
235
 
198
- html = wrappingEntryTag && elementTagName ? `
236
+ html =
237
+ wrappingEntryTag && elementTagName
238
+ ? `
199
239
  <${elementTagName}>
200
240
  ${serialize(elementInstance)}
201
241
  </${elementTagName}>
202
242
  `
203
- : serialize(elementInstance);
243
+ : serialize(elementInstance);
204
244
  } else {
205
245
  console.warn('WARNING: No custom element class found for this entry point.');
206
246
  }
207
247
 
208
248
  return {
209
249
  html,
210
- metadata: definitions
250
+ metadata: definitions,
211
251
  };
212
252
  }
213
253
 
@@ -223,11 +263,8 @@ async function renderFromHTML(html, elements = []) {
223
263
 
224
264
  return {
225
265
  html: serialize(finalTree),
226
- metadata: definitions
266
+ metadata: definitions,
227
267
  };
228
268
  }
229
269
 
230
- export {
231
- renderToString,
232
- renderFromHTML
233
- };
270
+ export { renderToString, renderFromHTML };