Giter VIP home page Giter VIP logo

wcc's Introduction

Web Components Compiler (WCC)

Netlify Status GitHub release GitHub Actions status GitHub license

Experimental Web Components compiler. It's Web Components all the way down! 🐢

How It Works

  1. Write a Web Component
    const template = document.createElement('template');
    
    template.innerHTML = `
      <style>
        .footer {
          color: white;
          background-color: #192a27;
        }
      </style>
    
      <footer class="footer">
        <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
      </footer>
    `;
    
    class Footer extends HTMLElement {
      connectedCallback() {
        if (!this.shadowRoot) {
          this.attachShadow({ mode: 'open' });
          this.shadowRoot.appendChild(template.content.cloneNode(true));
        }
      }
    }
    
    export default Footer;
    
    customElements.define('wcc-footer', Footer);
  2. Run it through the compiler
    import { renderToString } from 'wc-compiler';
    
    const { html } = await renderToString(new URL('./path/to/component.js', import.meta.url));
  3. Get HTML!
    <wcc-footer>
      <template shadowrootmode="open">
        <style>
          .footer {
            color: white;
            background-color: #192a27;
          }
        </style>
    
        <footer class="footer">
          <h4>My Blog &copy; 2022</h4>
        </footer>
      </template>
    </wcc-footer>

Installation

WCC runs on NodeJS and can be installed from npm.

$ npm install wc-compiler --save-dev

CommonJS

If you need CommonJS support, a separate pre-bundled (with Rollup) distribution of WCC is available at dist/wcc.dist.cjs. Example:

const { renderToString } = require('wc-compiler/dist/wcc.dist.cjs');

Documentation

See our website for API docs and examples.

Motivation

WCC is not a static site generator, framework or bundler. It is focused on producing raw HTML from Web Components with the intent of being easily integrated into a site generator or framework, like Greenwood or Eleventy, the original motivation for creating this project.

In addition, WCC hopes to provide a surface area to explore patterns around streaming, serverless and edge rendering, and as acting as a test bed for the Web Components Community Groups's discussions around community protocols, like hydration.

wcc's People

Contributors

thescientist13 avatar aholtzman avatar

Stargazers

Joey avatar David Burles avatar Mohamed Akram avatar Julian Cataldo avatar Marco Beierer avatar saŭlo avatar Murad avatar Dmitri Pavlutin avatar Chris Nelson avatar  avatar Val Packett avatar Alice Pote avatar Artur Parkhisenko avatar South Drifted avatar Jeff Caldwell avatar Bryan Ollendyke avatar Russell Perkins avatar Danny Glidewell avatar Rustie avatar Sunny Singh avatar Craig Mulligan avatar  avatar Evan J. Nee avatar Jeff Carpenter avatar Vladislav Ponomarev avatar Brad Pillow avatar Abdurrahman Fadhil avatar Rafael Bardini avatar Freddy Alvarado R avatar Pavel avatar Connor Bär avatar Yuvaraj Thiagarajan avatar Richard Hess avatar Jan Andrle avatar Nikita avatar Mark Malstrom avatar Iced Quinn avatar Stefan Kopco avatar Andrew Chou avatar Alex Bates avatar Jurriaan avatar Boticello avatar Bob avatar Glenn 'devalias' Grant avatar Raúl Romo avatar Karl Herrick avatar Bruce B. Anderson avatar João Castro avatar Bartosz Adamczyk avatar Luke Harris avatar Andrejs Agejevs avatar Rob Letts avatar Joel Moss avatar loczek avatar  avatar Mundi Morgado avatar Nathan Knowler avatar Duc-Thien Bui avatar Nick Hehr avatar Brian Perry avatar Bruno Scherer avatar Brandon Zhang avatar xsf avatar Mark Hoad avatar Winston Fassett avatar Matthew Waldron avatar Dennis avatar Elliott Marquez avatar Shawn Allen avatar Arek Bartnik avatar Bruno Stasse avatar Thomas Digby avatar fro avatar Wesley Luyten avatar Keith Cirkel avatar mohammed abdulrahman idrees avatar Grant Hutchinson avatar  avatar

Watchers

Richard Hess avatar  avatar Grant Hutchinson avatar Paul Barry avatar  avatar  avatar

wcc's Issues

Routing / Animations / Transitions

Type of Change

  • New Feature Request

Summary

More of a catch all, but want to track something high level around exploring how this sort of functionality / behavior could be supported in wcc, if at all. Maybe not, but it would be fun to explore and try things out to see what is possible, at least at the userland level.

Details

Some considerations / use cases

  • An MPA that can "hold the frame", think like a global music player on a site
  • Prefetching
  • Animations / Transitions API

Maybe when wcc returns metadata, it can be in the shape of the dependency graph? This may provide hints to users how their code should be loaded / anticipated based by what is showing on the page or could be visible. 🤔

inline event handling

Type of Change

  • Documentation / Website

Summary

Want to make sure that this pattern applies (assuming it should) to help ensure declarative event handling, e.g.

class Header extends HTMLElement {
  
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = this.render();
    }
  }

  toggle() {
    alert('this.toggle clicked!');
  }

  render() {
    return `
      <header class="header">
        <button onClick="${this.toggle()}">Button To Click</button>
      </header>
    `;
  }
}

export { Header };
customElements.define('wcc-header', Header);

Details

The current example looks like this, which is more imperative.

class Header extends HTMLElement {
  
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = this.render();
    } else {
      const button = this.shadowRoot.querySelector('button');

      button.addEventListener('click', this.toggle);
    }
  }

  toggle() {
    alert('this.toggle clicked!');
  }

  render() {
    return `
      <header class="header">
        <button>Button To Click</button>
      </header>
    `;
  }
}

export { Header };
customElements.define('wcc-header', Header);

That said, I'm not sure if one is more or less useful for something like #11 ? Because if we want to strip out "dead code", which one is easer to detect via AST?

standardize HTML response between `renderToString` and `renderFromHTML`

Type of Change

  • Enhancement

Summary

There is a slight behavior difference between renderToString and renderFromHTML in that the former uses parseFragment and the parse, from the HTML parsing library.

<!-- input -->
const artists = ['Analog', 'FAVE', 'Silverteeth'];
const { html } = await renderFromHTML(`
    ${artists.map(artist => {
      return `
        <h2>${artist}</h2>
      `;
  }).join('')}
<!-- output -->
<html><head></head><body>
  <h2>Analog</h2>
  <h2>FAVE</h2>
  <h2>Silverteeth</h2>
  
  </body></html>

It seems that in practice, parse will automatically return a full HTML document, including <html> and <head> tags.

Details

Not sure if there is a clear difference between the two or if I can just safely use parseFragment in all cases? From my testing, I didn't have to pass a valid document structure to parse though. Anyway, should probably avoid adding output for users and stick to just what was passed in, and make the output of all these APIs consistent.

`async` AST visitor functions causing race conditions

Type of Change

  • Bug

Summary

Observed in #73 that I was starting to get race conditions when recursively trying to serialize nested JSX elements that would intermittently lead to this error from this line

<todo-app>
  <todo-list></todo-list>
</todo-app>

```sh
TypeError: Cannot destructure property 'moduleURL' of 'definitions[tagName]' as it is undefined.

The issue was the compiler was already trying to serialize the HTML of <todo-app> before <todo-list> had been defined.

Details

I think I did this in Greenwood too but not sure I thought visitor functions could be async? I think with the introduction of JSX transformations which might slow the process down a little bit, these async variations were letting things happen out of order, which WCC can't support, at least not at this time. We need to render things top down and define the elements before we can use them. Doing that out of order is going to lead to issues.


That said, we should try and figure this problem out some how. - #76

use fixtures / snapshots for specs instead of JS DOM

Type of Change

  • Other (please clarify below)

Summary

In our specs currently, we convert the HTML from wcc into DOM and query it via JSDOM

So for this component

const template = document.createElement('template');

template.innerHTML = `
  <footer class="footer">
    <h4>
      <a href="https://www.greenwoodjs.io/">My Blog &copy;${new Date().getFullYear()} &#9672 Built with GreenwoodJS</a>
    </h4>
  </footer>
`;

class Footer extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export default Footer;

customElements.define('wcc-footer', Footer);

We would test it like so

import chai from 'chai';
import { JSDOM } from 'jsdom';
import { renderToString } from '../../../src/wcc.js';

const expect = chai.expect;

describe('Run WCC For ', function() {
  const LABEL = 'Single Custom Element w/ Declarative Shadow DOM';
  let dom;

  before(async function() {
    const { html } = await renderToString(new URL('./src/footer.js', import.meta.url));
    
    dom = new JSDOM(html);
  });

  describe(LABEL, function() {
      
    it('should have one top level <template> with an open shadowroot', function() {
      expect(dom.window.document.querySelectorAll('template[shadowroot="open"]').length).to.equal(1);
      expect(dom.window.document.querySelectorAll('template').length).to.equal(1);
    });

    describe('<footer> component and content', function() {
      let footer;

      before(async function() {
        footer = new JSDOM(dom.window.document.querySelectorAll('template[shadowroot="open"]')[0].innerHTML);
      });

      it('should have one <footer> tag within the <template> shadowroot', function() {
        expect(footer.window.document.querySelectorAll('footer').length).to.equal(1);
      });
  
      it('should have the expected content for the <footer> tag', function() {
        expect(footer.window.document.querySelectorAll('h4 a').textContent).to.contain(/My Blog/);
      });
    });

  });
});

But I'm curious to see if would be more pragmatic to just use fixtures instead? In other words, like in snapshot testing, capture the expected outcome as an HTML file, and just match the response to the file on disk.

Details

So for the above component test, instead of testing via DOM, we just test against a fixture file in the test directory

<wcc-footer>
  <template shadowroot="open">
    <footer class="footer">
      <h4>
        <a href="https://www.greenwoodjs.io/">My Blog &copy;${new Date().getFullYear()} &#9672 Built with GreenwoodJS</a>
      </h4>
    </footer>
  </template>
</wcc-footer>

I think it's easier to reason about and easier to edit, and should make writing tests a LOT easier. (IMO)

`renderToString` should wrap rendered HTML in custom element tag definition

Type of Change

  • Enhancement

Summary

Currently the response from renderToString does not include the wrapping tags of the custom element being rendered, only the contents of rendering it.

const template = document.createElement('template');

template.innerHTML = `
  <footer class="footer">
    <h4>
      <a href="https://www.greenwoodjs.io/">My Blog &copy;${new Date().getFullYear()} &#9672 Built with GreenwoodJS</a>
    </h4>
  </footer>
`;

class Footer extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export default Footer;

customElements.define('wc-footer', Footer);

And used like this

  const { html: footer } = await renderToString(new URL('./footer.mjs', import.meta.url));

will return the following HTML

<template shadowroot="open">
  <footer class="footer">
    <h4>
      <a href="https://www.greenwoodjs.io/">My Blog ©2022 ◈ Built with GreenwoodJS</a>
    </h4>
  </footer>
</template>

There is no <wcc-footer>...</wcc-footer/>

Related to #18

Details

So I guess, is there ever a reason not to return it? I could maybe seeing an option to omit it, like if you are doing the static example linked above for our docs/?

lift component level CSS to `<head>` for documentation site

Type of Change

  • Documentation / Website

Summary

Now that patterns for non Declarative Shadow DOM based rendering are emerging within the project via #49 and innerHTML is an option, it would probably be better for the documentation site if the component level <style> tags were lifted to the <head> of the document.

Details

The main goal here would be to help alleviate in instances of FOUC since each of those inline <style> tags is likely going to be render blocking.

add support for light DOM children and `<slot>`s

Type of Change

  • New Feature Request

Summary

Children elements, like for <slot>, should be supported within the compiler.

Details

Example
With a template like this

<div class="gallery>
  <button>Previous</button>
  <slot></slot>
  <button>Next</button>
</div>

And usage like this

<my-carousel>
  <img src="/assets/image1.png"/>
  <img src="/assets/image2.png"/>
  <img src="/assets/image3.png"/>
</my-carousel>

Would generate this

<my-carousel>
  <template shadowroot="open">
    <div class="gallery">
      <button>Previous</button>
      <slot></slot>
      <button>Next</button>
    </div>/
  </template>
  <img src="/assets/image1.png"/>
  <img src="/assets/image2.png"/>
  <img src="/assets/image3.png"/>
</my-carousel>

add test case for bare import specifiers

Type of Change

  • Other (please clarify below)

Summary

As part of #44 , I tried adding test cases to the PR, but they kept failing, and I think it is related to #24 .

Details

Would like to add a test case though to ensure this works as expected ASAP. Here is what I had originally - #44

Make sure to install node-fetch as a devDependency

// ./components/events-list.js
import fetch from 'node-fetch';

class EventsList extends HTMLElement {
  async connectedCallback() {
    if (!this.shadowRoot) {
      // TODO should probably try and make this a JSON file
      const events = await fetch('http://www.analogstudios.net/api/v2/events').then(resp => resp.json());

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<h1>Events List (${events.length})</h1>`;
    }
  }
}

export {
  EventsList
};

customElements.define('wc-events-list', EventsList); 
// index.js
import './components/events-list.js';

export default class HomePage extends HTMLElement {
  constructor() {
    super();

    if (this.shadowRoot) {
      // console.debug('HomePage => shadowRoot detected!');
    } else {
      this.attachShadow({ mode: 'open' });
    }
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = this.getTemplate();
  }

  getTemplate() {
    return `
      <wc-events-list></wc-events-list>
    `;
  }
} 
// node-modules.spec.js
/*
 * Use Case
 * Run wcc against a custom element using a dependency from node-modules
 *
 * User Result
 * Should run without error.
 *
 * User Workspace
 * src/
 *   components/
 *     events-list.js
 *   index.js
 */
import { renderToString } from '../../../src/wcc.js';

describe('Run WCC For ', function() {
  const LABEL = 'Custom Element w/ a node modules dependency';

  describe(LABEL, function() {
    it('should not fail when the content load a node module', function() {
      Promise.resolve(renderToString(new URL('./src/index.js', import.meta.url)));
    });
  });
}); 

partial hydration (side effect free custom elements, e.g. tree shaking)

Type of Change

  • New Feature Request

Summary

To build off of #3 , the next level of granularity to try and achieve in hydration strategies is partial hydration, which is

Use knowledge of the client vs server to only ship code / serialize data needed in the browser.

Details

Which I basically interpret to mean that given a component like this

class Header extends HTMLElement {
  constructor() {
    super();

    if (this.shadowRoot) {
      const button = this.shadowRoot.querySelector('button');

      button.addEventListener('click', this.toggle);
    }
  }

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = this.render();
    }
  }

  toggle() {
    alert('this.toggle clicked!');
  }

  render() {
    return `
      <style>
        .header {
          background-color: #192a27;
          min-height: 30px;
          padding: 10px;
          font-size: 1.2rem;
        }

        ...
      </style>

      <header class="header">
        ...
      </header>
    `;
  }
}

export { Header };
customElements.define('wcc-header', Header);

Since there are no props or state, we should be able to infer

  • Only one render is needed, which can be handled via declarative shadow dom
  • We still need the event handler

So in an ideal world, this is what would actually be shipped to the browser

class Header extends HTMLElement {
  constructor() {
    super();

    if (this.shadowRoot) {
      const button = this.shadowRoot.querySelector('button');

      button.addEventListener('click', this.toggle);
    }
  }

  toggle() {
    alert('this.toggle clicked!');
  }
}

export { Header };
customElements.define('wcc-header', Header);

Agnostic (Edge) Runtime support

Type of Change

  • New Feature Request

Summary

To maximize the potential and viability of wcc across as many runtimes as possible, an effort should be undertaken to ensure support across "standards" based serverless and edge runtime environments. Serverless environments are a little more forgiving, but edge functions typically cannot not use things like Node's fs or CommonJS. However, they do (plan to) standardize on Web APIs like fetch, Request and Response.

Details

Basically need to evaluate and allow wcc to be usable outside of just NodeJS. That will mean validating our dependencies for their module system, and any usage of Node specific APIs like fs. Perhaps this may require a pre-bundle if there is an expectation of ESM only as well, but we will have to see.

I think it would be great to reach

  • Lambda@Edge
  • Netlify Edge Functions
  • Cloudflare Workers
  • Vercel

support configurable `shadowroot` attribute for `<template>` tags

Type of Change

  • New Feature Request

Summary

Coming out of #63 (comment), just wanted to track the discussion around how to actually support the shadowroot attribute for <template> tags as I'm not super clear on is how instances of <template> tags (HTMLTemplateElement) and ShadowRoot work together, specifically around the shadowroot="open" attribute and mode.

Specifically, as per that PR, when creating a template, a template returns its contents inside <template> tags, eg.

<wcc-footer>
  <template shadowroot="open">
    <footer class="footer">
      <h4>My Blog &copy; 2022</h4>
    </footer>
  </template>
</wcc-footer>

And when instantiating a Shadow Root in a custom element, we can set the mode that way, e.g.

class Footer extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

So how does a template get access to the mode, which we would want so as to properly set the shadowroot attribute? Presumably some users may want the option of open / closed, and this may also impact how we implement #16. Also, these two are the same things, right?

Details

My understanding is shadowRoot and HTMLTemplateElement are different class hierarchies (sub classes) from what I understand, so not sure what option to pursue here? I can think of a couple options at least just to get the ball rolling

  1. User sets the attribute manually on a <template>
    const template = document.createElement('template');
    
    template.innerHTML = `    
      <footer class="footer">
        <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
      </footer>
    `;
    
    template.setAttribute('shadowroot', 'open');
  2. Have the compiler check for mode when serializing and update it that way
    async function renderComponentRoots(tree) {
      for (const node of tree.childNodes) {
        if (node.tagName && node.tagName.indexOf('-') > 0) {
          ...
          
          const elementHtml = elementInstance.shadowRoot
            ? elementInstance.getInnerHTML({ includeShadowRoots: true })
            : elementInstance.innerHTML;
          const elementTree = parseFragment(elementHtml);
    
          // if we have a shadowRoot, set the `<template>` shadowroot attribute according to shadowRoot.mode
          if(elementInstance.shadowRoot) {
            const mode = elementInstance.shadowRoot.mode;
            //  do something with the tree and set the attribute on the `<template>` accordingly
          }
    
          ...
        }
    
        ...
      }
    
      return tree;
    }

I'm thinking option #2 seems pretty feasible. Also, not sure if this means we need a get innerHTML for HTMLTemplateElement too?

opt out of top level wrapping entry tag name when using `renderToString`

Type of Change

Feature

Summary

With renderToString currently, if you have a custom element that has a customElements.define call in it, e.g.

export default class MyComponent extends HTMLElement {
  // ...
}

customElements.define('my-component', MyComponent);

The HTML string produced will come out with a wrapping tag for MyComponent

<my-component>
  <!-- rendered html -->
</my-component>

Details

The issue is two fold

  1. if you're working in a system that will create single artifacts / bundles (like Serverless / Edge) functions
  2. And you're have other components you import with their own customElements.define statement

Even if you have no define call for MyComponent

import './foo.component.js';

export default class MyComponent extends HTMLElement {
  // ...
}

Your bundled code is going to have something like this

import './foo.component.js';

export class FooComponent extends HTMLElement {
  // ...
}

customElements.define('foo-component', FooComponent);

export default class MyComponent extends HTMLElement {
  // ...
}

The rendered HTML will come out like this now, because WCC just looks for the first customElements.define call it sees in the file

<foo-component>
  <my-component>
    <!-- rendered HTML -->
  </my-component>
</foo-component>

So in some cases, the wrapping my be warrented / desired, in other cases not. Probably best to give control to that so in bundling / single file cases, the final output can be better managed

`window` / `globalThis` handling

Type of Change

  • New Feature Request

Summary

Coming out of ProjectEvergreen/greenwood#992, saw instances where code that was getting SSR'd in WCC was failing due to window not being defined, like in the Greenwood router.

ReferenceError [Error]: window is not defined
    at file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/packages/cli/src/lib/router.js:45:1
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:533:24)
    at async initializeCustomElement (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/src/wcc.js:139:8)
    at async renderFromHTML (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/src/wcc.js:193:5)
    at async executeRouteModule (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/packages/cli/src/lib/ssr-route-worker.js:17:22)
    at async MessagePort.<anonymous> (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/packages/cli/src/lib/ssr-route-worker.js:47:3)
error Command failed with exit code 1.

Details

We currently use globalThis in the dom shim, but don't test for it specifically, or call it out clearly that it should be the convention, so let's do that here. We probably shouldn't expose window specifically, but rather encourage anyone writing isomorphic code to use globalThis instead.

The globalThis property provides a standard way of accessing the global this value (and hence the global object itself) across environments. Unlike similar properties such as window and self, it's guaranteed to work in window and non-window contexts. In this way, you can access the global object in a consistent manner without having to know which environment the code is being run in. To help you remember the name, just remember that in global scope the this value is globalThis.

Go Live checklist

Type of Change

  • Other (please clarify below)

Summary

Just tracking list of tasks and TODOs to make this repo public

Details

  1. Basic docs and README - #9
  2. Get website domain name (github pages?) - I think using Netlify is fine for now
  3. Acquire npm publishing name (files) - #26
  4. Clean up TODOs + commented code - #26
  5. Contributing Guide - #26
  6. Examples - #54
    • SSG (innerHTML), this website
    • progressive hydration
    • SSR
    • data loading w/ counter example
  7. Logo + favicon + open graph image
  8. Run Lighthouse for perf / a11y audit, <meta> tags - #26
  9. Final styles - #20
  10. Website responsiveness - #57
    Screenshot 2022-05-23 at 21 43 42
  11. Final README walk through - #54
  12. Transfer to ProjectEvergreen and make public

Nice To Have

  1. Table of Contents for Docs and Example pages
  2. PrismJS for code samples - #26
  3. Pull simple.css from npm / node modules - #26

Post Launch

  1. Add to projectevergreen.github.io projects page / personal blog post

Streaming HTML

Type of Change

  • New Feature Request

Summary

Would like to explore how to provide a streaming API that could also break apart the page at render roots.

So for example a page like this could come out in multiple flushes

<body>
  <my-header></my-header>
  <my-app></my-app>
  <my-footer></my-footer>
</body>

Would flush across those component definitions / boundaries / islands.

Details

To go even further, maybe we could recursively stream nested roots? Like in this example
wcc-architecture

Links / References

investigate requirement of needing a `default export` for rendering entry points

Type of Change

  • Question / Documentation

Summary

Currently, wcc requires a default export from the first entry point custom element definition it parses.

For example, is you do this

// src/index.js
import './components/footer.js';
import './components/header.js';

const template = document.createElement('template');

template.innerHTML = `
  <wcc-header></wcc-header>

  <h1>Hello!</h1>

  <wcc-footer></wcc-footer>

  </main>
`;

class Home extends HTMLElement {

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export { Home };
const { html } = await renderToString(new URL('./src/index.js', import.meta.url));

wcc will throw this error

file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:80
  const elementInstance = new element(data); // eslint-disable-line new-cap
                          ^

TypeError: element is not a constructor
    at initializeCustomElement (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:80:27)
    at async renderToString (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:99:27)
    at async init (file:///Users/owenbuckley/Workspace/github/repos/wcc/build.js:38:22)

Details

Maybe it's because this doesn't call customElements.define, so we have no way to know the name? Perhaps that's the tradeoff

// THIS
customElement.define('wcc-header', Header);

// OR THIS?
export default Home

Not sure if this is the same thing, or supplemental to #117 so should review them both as part of doing either these tasks.


Somewhat related, we should better handle the case where you use a custom element, but not import it

// NO FOOTER IMPORT, but `wcc-footer` is still used in the HTML
import './components/header.js';

const template = document.createElement('template');

template.innerHTML = `
  <wcc-header></wcc-header>

  <h1>Hello!</h1>

  <wcc-footer></wcc-footer>
`;

class Home extends HTMLElement {

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export { Home };

which will casue wcc to throw this error

[0] file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:23
[0]         const { moduleURL } = deps[tagName];
[0]                 ^
[0]
[0] TypeError: Cannot destructure property 'moduleURL' of 'deps[tagName]' as it is undefined.
[0]     at renderComponentRoots (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:23:17)
[0]     at renderComponentRoots (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:36:13)
[0]     at renderComponentRoots (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:36:13)
[0]     at async renderToString (file:///Users/owenbuckley/Workspace/github/repos/wcc/src/wcc.js:107:21)
[0]     at async file:///Users/owenbuckley/Workspace/github/repos/wcc/build.js:16:28

make JSX transformations (and dependencies) opt-in / lazy loaded

Type of Change

Feature

Summary

It would be nice if the opt-in feature of authoring in JSX could be more intentional in that specific dependencies could be avoided on install / consumption unless needed.

Detail

For example, a "vanilla" WCC implementation without JSX does not require

  • acorn-jsx
  • escodegen

Would be good to find a way to make their installation and usage within the code more opt-in. Also, as observed in ProjectEvergreen/greenwood#972, it might nice to refactor jsx-loader.js so escodegen is not a leaky abstraction.

refactor how definitions and entry points are determined (leverage `default export`)

Type of Change

Enhancement / Bug

Summary

As observed in #114 coming over from ProjectEvergreen/greenwood#1079, the current design of WCC in particular around determining metadata for entry points, conveniently wrapping renderToString calls in a wrapping HTML element starts to fall apart when multiple customElement.define statements are in a single file.

So given this input

class Navigation extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/about">About</a></li>
          <li><a href="/artists">Artists</a></li>
        <ul>
      </nav>
    `;
  }
}

customElements.define('wcc-navigation', Navigation);

class Header extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <header class="header">>
        <h4>My Personal Blog</h4>
      </header>
    `;
  }
}

export default Header;

This is the current metadata output

{
  html: '\n' +
    '      <wcc-navigation>\n' +
    '        \n' +
    '      <header class="header">&gt;\n' +
    '        <h4>My Personal Blog</h4>\n' +
    '      </header>\n' +
    '    \n' +
    '      </wcc-navigation>\n' +
    '    ',
  metadata: [
    'wcc-navigation': {
      instanceName: 'Navigation',
      moduleURL: [URL],
      source: 'class Navigation extends HTMLElement {\n' +
        '    connectedCallback() {\n' +
        '        this.innerHTML = `\n' +
        '      <nav>\n' +
        '        <ul>\n' +
        '          <li><a href="/">Home</a></li>\n' +
        '          <li><a href="/about">About</a></li>\n' +
        '          <li><a href="/artists">Artists</a></li>\n' +
        '        <ul>\n' +
        '      </nav>\n' +
        '    `;\n' +
        '    }\n' +
        '}\n' +
        "customElements.define('wcc-navigation', Navigation);\n" +
        'class Header extends HTMLElement {\n' +
        '    connectedCallback() {\n' +
        '        this.innerHTML = `\n' +
        '      <header class="header">>\n' +
        '        <h4>My Personal Blog</h4>\n' +
        '      </header>\n' +
        '    `;\n' +
        '    }\n' +
        '}\n' +
        'export default Header;',
      url: [URL],
      isEntry: true
    }
  ]
}

Details

As we can see <wcc-navigation> is considered the entry point, though technically that should belong to original moduleURL passed in, right?

Also, technically isn't the default export always going to be the entry point? This seems to substantiate #23 I suppose?
So it seems like the default export should be the entry point. And from there, if the class definition aligns with a customElements.define call, then that should decided if it is safe to wrap, or at least what the right tag to use is if wrapping is still desired.

Another call out though is that if there is only export default XXXX but no customElements.define how do we tracking this in metadata? Currently we use the tag name as a key, so will need to solve for that too.

Not sure if this is the same thing, or supplemental to #23 so should review them both as part of doing either these tasks.


This an acknowledged defect, but also requires a bit of rearchitecting I think so flagging as both since I don't the final solution here could be reasonably published as a "patch" fix in the spirit of the definition.

Coarse Grained (Inferred) Observability for JSX

Type of Change

  • New Feature Request

Summary

Related to #84 , it would be cool if for something like this

class TodoListItem extends HTMLElement {
  constructor() {
    super();
    this.todo = {};
  }

  render() {
    const { completed, task } = this.todo;
    const completionStatus = completed ? '✅' : '⛔';
    
    return (
      <span>
        {task} <span>{completionStatus}</span>
        
         <button onclick={() => this.dispatchDeleteTodoEvent(this.todo.id)}></button>          
      </span>
    );
  }
}

customElements.define('todo-list-item', TodoListItem);

Details

So the compiled output would look something like this

class TodoListItem extends HTMLElement {
  constructor() {
    super();
    this.todo = {};
  }

  static get observedAttributes () {
    return ['todo'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (newValue !== oldValue) {
      this.render();
    }
  }

  render() {
    ...
  }
}

customElements.define('todo-list-item', TodoListItem);

In this case we can automatically infer generate the observedAttributes based on what is getting used in the render function, and then from there compute the an attributeChangedCallback function. 🤩

Details

To be fair, the reason this is considered coarse-grained is because attributeChangedCallback just reruns the render function completely, so all the (inner) HTML is blown out, so not very efficient, but certainly helpful nonetheless IMO!

But... as the name also implies, this means that there could / should be room for fine-grained observability as well, such that we can the compiled knowledge about the template and component and instead run an update function that knows hows to more tactically call textContent or setAttribute to the specific DOM node, rather than just re-computing innerHTML. One thing at a time though. 😅

Form Controls for JSX

Type of Change

  • New Feature Request

Summary

Would like to support these kinds of features

  1. Boolean Attributes
  2. Form Submission

Details

Boolean Attributes

Like a checked attribute

render() {
  const { completed, task } = this.todo;
  const checked = completed ? 'checked' : '';

  return (
    <input class="complete-todo" type="checkbox" checked/>
  );
}

To my knowledge, JSX does not support this syntax

<input type="checkbox" {isChecked} />

Form Submission

Would like be able to do something like this

class Todo extends HTMLElement {
  addTodo(e) {
    e.preventDefault();
    ...

    this.render();
  }

  render() {
    return 
      <form onsubmit={this.addTodo}>
        <input class="todo-input" type="text" placeholder="Food Shopping" required/>
        <button class="add-todo" type="button" onclick={this.addTodo}>+ Add</button>
      </form>
    );
  }
}

Right now it works, but by accident I think, and seeing this error in the console
Screen Shot 2022-08-02 at 3 30 58 PM

`HydrateElement` base class prototyping

Type of Change

  • Other

Summary

Initially started in ProjectEvergreen/greenwood#548, but wcc may provide a nice sandbox for something testing something like this out.

Latest prototype looks something like this

// https://web.dev/declarative-shadow-dom/#hydration
class HydrateElement extends HTMLElement {
  // attach the shadow
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

/*
<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>
*/

export { HydrateElement };

Details

Some other ideas / feature that have come up while working on wcc that are worth tracking as part of this

support `innerHTML` in compiler for custom elements

Type of Change

  • New Feature Request

Summary

Although I did introduce a lightMode configuration option for escaping the Shadow DOM <template shadowroot="open">, like for more "static" scenarios (e.g. documentation site), I've been thinking more about this implementation detail.

Specifically, I just realized now a better way to support this from userland would be to just let users pick which one works best for them? So if you want to just ship static content, use innerHTML, if it is a more interactive scenario, and say you plan to use hydration, then use Shadow DOM.

Details

This is important because browsers don't render a <template> tag, so anything inside them, like text content, will not be rendered without either using a browser that supports Declarative Shadow DOM, or by adding the polyfill.

This could also help play nicely with global CSS libraries like Tailwinds.

Will need to consider:

  • If lightMode makes sense anymore?
  • Document that without Shadow DOM, you can't use :host and <slot> etc.
  • Is #16 relavant?

DOM shim not accurately creating templates (`HTMLTemplateElement`)

Type of Change

  • Bug

Summary

Implementation of document.createElement('template') in dom-shim.js is incorrect. Based on testing in Chrome, the operation of creating a new template should include the <template> tag as part of the content, and would be present say when appending to another element.

Screen Shot 2022-06-05 at 2 22 13 PM
Screen Shot 2022-06-05 at 2 16 14 PM

Details

So doing something like this shouldn't work as we have it now

const template = document.createElement('template');

template.innerHTML = `
  <footer>
    <p>
      <a href="https://projectevergreen.github.io">WCC &#9672 Project Evergreen</a>
    </p>
  </footer>
`;

class Footer extends HTMLElement {
  connectedCallback() {
    this.appendChild(template.content.cloneNode(true));
  }
}

export {
  Footer
};

customElements.define('wcc-footer', Footer);

As it will yield this HTML, which is wrong because the <template> tag should be included.

<wcc-footer>
  <footer>
    <p>
      <a href="https://projectevergreen.github.io/">WCC ◈ Project Evergreen</a>
    </p>
  </footer>
</wcc-footer>

provide component data through render method params (constructor props)

Type of Change

Feature

Summary

As part of ProjectEvergreen/greenwood#1157, an alternative to getData was identified that could be more useful at the framework level as getData is more of a component / entry point level mechanic, but frameworks will likely have / want to implement their own data loading mechanics, so being able to DI data from the framework would be nice. For example, Greenwood would like to be able to pass in an instance of a Request object, and possibly additional framework metadata.

On top of that frameworks, may want to define their own naming convention for data loading, the most common one being the loader pattern, e.g.

export default class PostPage extends HTMLElement {
  constructor(request) {
    super();
    
    const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
    this.postId = params.get('id');
  }

  async connectedCallback() {
    const { postId } = this;
    const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
    const { id, title, body } = post;

    this.innerHTML = `
      <h1>Fetched Post ID: ${id}</h1>
      <h2>${title}</h2>
      <p>${body}</p>
    `;
  }
}

Details

In theory a framework could import / call getData itself as a generic loader pattern, WCC would still run it itself too as part of rendering, incurring a double fetch. Also this makes the name flexible, and could let frameworks take on more complex requirements like ProjectEvergreen/greenwood#880.

So instead of relying only on WCC to do the data fetching and orchestration, we could pass these "constructor props" to WCC and it can inject that data into the top level component of the entry point, e.g.

const request = new Request({ /* ... */ });
const { html } = await renderToString(new URL(moduleUrl), false, request);

This may beg the question if WCC should even have a data loading mechanic, so it might be good to spin up an issue or discussion for that.

mark entry point custom element when returning `metadata`

Type of Change

  • New Feature Request

Summary

It would be great for returned metadata to mark the initial custom element as the entry point, which can be helpful for more app like usages of WCC.

Details

For example

const { metadata } = await renderToString(new URL('./src/pages/home.js', import.meta.url));

/*
 * {
 *   metadata: [
 *     'wcc-home': { instanceName: 'HomePage', moduleURL: [URL], entry: true },
 *     'wcc-footer': { instanceName: 'Footer', moduleURL: [URL] },
 *     'wcc-header': { instanceName: 'Header', moduleURL: [URL] },
 *     'wcc-navigation': { instanceName: 'Navigation', moduleURL: [URL] }
 *   ]
 * }
 *

However, if there is no tagName provided, like for a layout route or page, the next level of custom elements should be used instead.

`renderFromHTML`

Type of Change

  • New Feature Request

Summary

Would like to introduce an API where the entry point can be a fragment of HTML instead of a custom element (JS)

Details

For example

import { renderToHTML } from 'wcc';

const { html } = await renderToHTML(`
  <body>
    <wcc-header></wcc-header/>

    <h1>Welcome to the Home Page</h1>

    <wcc-footer></wcc-footer/>
  <body>
`;

self defined progressive hydration for custom elements

Overview

Coming out of conversation around hydration within the WCCG, was thinking about what might be possible if we could intercept the customElements.define call at the browser level? Maybe we could use it to let a WebComponent bootstrap itself with its own intersection observer logic, so instead of having to defining everything via attributes and / or some framework glue layer, e.g

<my-element custom:dsl="x,y,z"></my-element>

A custom element could do this itself via a contract / community protocol, like using a static method or member, which could initialize its own Intersection Observer maybe?

class Counter extends HTMLElement {
  static __secret() {
    console.debug('sssshhh!  this is a secret :)')
    // do stuff with onload function, IntersectionObserver, Mutation Observer
  }
}

export { Counter }

customElements.define('wcc-counter', Counter)

However, this presumably introduces a chicken and the egg scenario though because at this point a CE is calling customElements.define, the JS would already have been loaded on to the point, which kind of defeats the purpose somewhat.

So there will still need to be a way to decouple the "intersection" trigger from the component itself, which I suppose does benefit a compiler based tool, so there is that. 😄

Details

My thought there is that for the above example, we could extract the static method from the class and just providing that to be include that at runtime.

async ExpressionStatement(node) {
  const { expression } = node;

  if(/* is custom element */) {
    const tagName = node.expression.arguments[0].value;
    const instanceName = node.expression.arguments[1].name;
    const module = (await import(moduleURL))[instanceName];
    const hydrate = module.hydrate || undefine;

    deps[tagName] = {
      instanceName
      moduleURL,
      hydrate
    }
  }
}

And then we inject just that function into the runtime. So in theory, there is no wcc overhead, it is purely user based.

What is neat about this I think, is that you can now open up levels and degrees of hydration aside from just top down which would be a limitation of having to express the attributes everywhere up front. So depending on how many

Additional thoughts / goals

  1. Make a good demo / use case
  2. See if we could take inspiration from from lazyDefine proposal
  3. Explore to see if Proxy's could be useful here
  4. Think about scaling as more CEs are added to the runtime via hydrate since this would push all that JavaScript to the runtime (a la Svelte). Which probably won't be much in the grand scheme of things, be entirely coming from userland, and entirely opt-in.
  5. Bonus points for stripping hydrate out of the custom elements class definition as well to avoid shipping the bytes at runtime

Opened an issue in the WCCG's Community Protocols repo.

using template literal backticks as quotes for custom element tag name breaks rendering

Type of Change

  • Bug

Summary

Using template string ` as quotes for the tag name in customElements.define definition "breaks" the compiler.

customElements.define(`wcc-footer`, Footer);

It will come out as it came in, un-serialized in the rendered HTML

<wcc-footer></wcc-footer>

Details

Example

const template = document.createElement('template');

template.innerHTML = `
  <footer class="footer">
    <h4>
      <a href="https://www.greenwoodjs.io/">My Blog &copy;${new Date().getFullYear()} &#9672 Built with GreenwoodJS</a>
    </h4>
  </footer>
`;

class Footer extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export default Footer;

customElements.define(`wcc-footer`, Footer);

FWIW, double quotes work fine

const template = document.createElement('template');

template.innerHTML = `
  <footer class="footer">
    <h4>
      <a href="https://www.greenwoodjs.io/">My Blog &copy;${new Date().getFullYear()} &#9672 Built with GreenwoodJS</a>
    </h4>
  </footer>
`;

class Footer extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export default Footer;

customElements.define("wcc-footer", Footer);

handle usage of unregistered custom elements

Type of Change

  • Enhancement

Summary

Given this custom element definition, where <wcc-navigiation><wcc-navigiation> is used but not included via import

const template = document.createElement('template');

template.innerHTML = `
  <header>
    <h1>Welcome to my website</h1>
    <wcc-navigation></wcc-navigation>
  </header>
`;

class Header extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

export { Header };

customElements.define('wcc-header', Header);

wcc will throw this error

yarn build
yarn run v1.22.17
$ node ./build.js
file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:16
      const { moduleURL } = definitions[tagName];
              ^

TypeError: Cannot destructure property 'moduleURL' of 'definitions[tagName]' as it is undefined.
    at renderComponentRoots (file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:16:15)
    at async renderComponentRoots (file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:26:7)
    at async renderComponentRoots (file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:26:7)
    at async renderComponentRoots (file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:26:7)
    at async renderToString (file:///Users/owenbuckley/Workspace/github/wcc/src/wcc.js:102:21)
    at async init (file:///Users/owenbuckley/Workspace/github/wcc/build.js:15:20)
error Command failed with exit code 1.

Details

It would be nice for wcc to try and handle this more gracefully, such as:
a) failing with a more friendly error, and produce the tag name, since we at least know that
b) "skipping" over it and showing a warning message in the console

I think b) (or a combination of a + b) is the way to go, and if so, perhaps this information that could also be updated in definitions as part of the returned metadata?


As a nice to have, where we are setting new custom elements in definitions, e.g.

definitions[tagName] = ...

We may want to check first ahead of running whatever logic got to that line, as a minimal optimization.

omit `<template>` from rendered declarative shadow dom (support static HTML workflows)

Type of Change

  • New Feature Request

Summary

As Declarative Shadow DOM is not supported yet across all browsers (Safari, FF), rendering something like this to one of those browsers

<wcc-footer>
  <template shadowroot="open">        
    <style>
      footer {
        bottom: 0;
        width: 100%;
        background-color: var(--accent);
        min-height: 30px;
        padding-top: 10px;
        grid-column: 1 / -1;
      }
  
      footer a {
        color: #efefef;
        text-decoration: none;
      }
  
      footer h4 {
        width: 90%;
        margin: 0 auto;
        padding: 0;
        text-align: center;
      }
    </style>

    <footer>
      <h4>
        <a href="https://projectevergreen.github.io">WCC ◈ Project Evergreen</a>
      </h4>
    </footer>

  </template>
</wcc-footer>

will result in missing content for that section of the HTML, because by definition, <template> tags are inert and not rendered by the browser.

Notice in the screenshot below, the Footer HTML is missing.
Screen Shot 2022-05-07 at 5 03 33 PM

Details

There is a polyfill for this but that obviously requires JS, but if you want to author / use Web Components using SSG / SSR, but ship entirely static content with no JS (not taking into account upcoming hydration strategies), the polyfill would be no go. So it would be nice to offer a configuration option to renderToString to break out of that "content" trap.

Another upside, is that this could make working with global CSS easier, since there would be no Shadow DOM anymore.

Potentially name it after includeShadowRoots?

add support for `addEventListener` as a no-op

Type of Change

  • New Feature Request

Summary

At the very least, we should be able to provide a no-op for addEventListener.

Details

So a case like this should not error at least

class MyComponent extends HTMLElement {
  constructor() {
    super();

    this.addEventListener('someCustomEvent', () => { console.log('it worked!'); });
  }

  connectedCallback() {
    this.innerHTML = '<h1>It worked!</h1>';
  }
}

assumed methods and members of custom element classes break the compiler

Type of Change

  • Bug

Summary

Through the compiler, we are assuming methods like connectedCallback and attachShadow which means any custom element that does not define these will break.

async function initializeCustomElement(elementURL, tagName, attrs = []) {
  ...
  
  await elementInstance.connectedCallback(); // what if the element does not define this method???

  return elementInstance;
}

Details

Basically, something like this "should" work just fine

export default class Empty extends HTMLElement { }

customElements.define('x-empty', Empty)

But the compiler will give errors instead

# missing `attachShadow`
<template shadowroot="${this.shadowRoot.mode}">
                                               ^
TypeError: Cannot read property 'mode' of null
   at Footer.getInnerHTML (file:///Users/owenbuckley/Workspace/github/repos/wcc/lib/dom-shim.js:68:49)
   at renderComponentRoots (file:///Users/owenbuckley/Workspace/github/repos/wcc/lib/wcc.js:24:48)
   at async renderToString (file:///Users/owenbuckley/Workspace/github/repos/wcc/lib/wcc.js:101:21)
   at async file:///Users/owenbuckley/Workspace/github/repos/wcc/ssg.js:16:28
error Command failed with exit code 1.

I'm not sure what the exact behavior should be or what can be assumed, but at the very least we should be able to handle gracefully, similar to #78 .

node modules (bare specifiers) are causing an unhandled filesystem exception

Type of Change

  • Bug

Summary

Testing in a side project, and a custom element like this

import '../components/card/card.native.js';
import fetch from 'node-fetch';

export default class ArtistsPage extends HTMLElement {
  async connectedCallback() {
    if (!this.shadowRoot) {
      const artists = await fetch('https://www.analogstudios.net/api/artists').then(resp => resp.json());
      const html = artists.map(artist => {
        return `
          <wc-card>
            <h2 slot="title">${artist.name}</h2>
            <img slot="image" src="${artist.imageUrl}" alt="${artist.name}"/>
          </wc-card>
        `;
      }).join('');

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = html;
    }
  }
}

will cause an unhandled exception trying to use fs.readFile on node-fetch

  moduleURL: URL {
    href: 'file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/www/pages/node-fetch',
    origin: 'null',
    protocol: 'file:',
    username: '',
    password: '',
    host: '',
    hostname: '',
    port: '',
    pathname: '/Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/www/pages/node-fetch',
    search: '',
    searchParams: URLSearchParams {},
    hash: ''
  }
}
(node:88140) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '/Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/www/pages/node-fetch'

Details

The issue is we blindly try and read all dependencies, even bare specifiers, which are typically node modules deps and should be left to the Node resolution algorithm and not walked with acorn.


That said, this may only be a patch, since I think in the context of #38 , will we have to support walking node modules? 🤔

custom (non JS) file extensions break the compiler

Type of Change

  • Bug

Summary

With upcoming features like experimental loaders from NodeJS (or eventually import assertions), WCC should avoid trying to parse files / extensions it isn't capable of handling when walking / parsing custom elements.

function registerDependencies(moduleURL, definitions) {
  const moduleContents = fs.readFileSync(moduleURL, 'utf-8');

  // if moduleURL is not JS, this will blow up!  💣 
  walk.simple(acorn.parse(moduleContents, {
    ecmaVersion: 'latest',
    sourceType: 'module'
  }), {
    ...
  })
}

Otherwise, acorn will blow up. 💥

SyntaxError [Error]: Unexpected token (2:8)
    at Parser.pp$4.raise (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:3453:13)
    at Parser.pp$9.unexpected (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:754:8)
    at Parser.pp$9.semicolon (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:731:66)
    at Parser.pp$8.parseExpressionStatement (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:1214:8)
    at Parser.pp$8.parseStatement (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:947:24)
    at Parser.pp$8.parseBlock (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:1230:21)
    at Parser.pp$8.parseStatement (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:912:36)
    at Parser.pp$8.parseTopLevel (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:811:21)
    at Parser.parse (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:583:15)
    at Function.parse (file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/node_modules/wc-compiler/node_modules/acorn/dist/acorn.mjs:633:35) {
  pos: 10,
  loc: { line: 2, column: 8 },
  raisedAt: 11
}

Details

For example, say wanting to use import for CSS

import css from './footer.css';

export default class FooterComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = this.getTemplate();
  }

  getTemplate() {
    const year = new Date().getFullYear();
    console.debug('FooterComponent.getTemplate', { packageJson });

    return `
      <style>
        ${css}
      </style>

      <footer class="footer">
        <h4>My Blog &copy;${year} </h4>
      </footer>
    `;
  }
}

customElements.define('app-footer', FooterComponent);

CSS solutions and support

Type of Change

  • Other (please clarify below)

Summary

Wanted to track some ideas of how CSS can be used, or what can be assumed isomorphic for both Node and browser contexts.

Details

Things to validate

  1. Using Node custom loaders to support import css from './styles.css'. In some browsers we should already have support for CSS Modules
  2. Constructable Stylesheets - I have never used them, so at least just want to make sure I'm not missing anything obvious here that would interrupt normal usage
  3. Other?

Perhaps option 1 could be leveraged to auto-inline CSS so in development you can have an external CSS, but not need it for production.

LInks / Resources / References

verify / ensure proper serialization of shadow roots excluding closed shadow roots from `getInnerHTML`

Type of Change

  • New Feature Request

Summary

Just want to make sure I'm properly handling HTMLElement.getInnerHTML for?

getInnerHTML(options = {}) {
  return options.includeShadowRoots
    ? `
      <template shadowroot="${this.shadowRoot.mode}">
        ${this.shadowRoot.innerHTML}
      </template>
    `
    : this.shadowRoot.innerHTML;
}

Details

Coming out of #19 and made a repo for testing, since it appears I might be taking some serious liberties with the implementation here. 😅
https://github.com/thescientist13/get-inner-html


Somewhat related to this, not sure if there is value in having a way to opt-out at the top level for this, like if your page is a custom element? Perhaps you want your page as light DOM, but still keep the shadow DOM for all nested children?

edit: to the above, I just recently pulled a feature called lightMode since I realized a better way to output non shadow content was to just let user's opt-out by using innerHTML. But in relation to this, if say someone is using a third party library and wants that library rendered without Shadow DOM (obviously mileage will vary vastly on this from lib to lib) but then that's a way to bring that config back?

`HTMLTemplateElement` should not have a `set innerHTML` method

Type of Change

  • Bug

Summary

Our current implementation for HTMLTemplateElement includes a setInnerHTML method.

class HTMLTemplateElement extends HTMLElement {
  constructor() {
    super();
    // console.debug('HTMLTemplateElement constructor');

    this.content = new DocumentFragment(this.innerHTML);
  }

  set innerHTML(html) {
    this.content.textContent = html;
  }
}

But according to MDN, that doesn't seem correct.

Details

The issue is I'm not sure where it goes? Without it, I'm not sure how setting innerHTML on a <template> and thus how to get that when using cloneNode? 🤔

`renderToString` is returning document tags within the `<template>` output when only rendering a fragment

Type of Change

  • Bug

Summary

After upgrading to 0.2.0, saw that when using renderToString, <html> and <head> tags are now coming back in the rendered HTML.

Seems like a regression due to #30 .

Details

Before

<wc-footer>
   <template shadowroot="open">

      <style>
        :host {
          color: var(--color-accent);
          bottom: 0;
          width: 100%;
          min-height: 30px;
          padding-top: 10px;
          text-align: center;
        }
    
        footer {
          background-color: #192a27;
        }
      </style>
    
      <footer>
        <span>© 2022</span>
      </footer>

  </template>
</wc-footer>

After

<wc-footer>
   <html><head><template shadowroot="open">

    <style>
      :host {
        color: var(--color-accent);
        bottom: 0;
        width: 100%;
        min-height: 30px;
        padding-top: 10px;
        text-align: center;
      }
  
      footer {
        background-color: #192a27;
      }
    </style>
  
    <footer>
      <span>© 2022</span>
    </footer>

  </template></head><body></body></html>
</wc-footer>

Also seems like we are missing the closing > as well. I bet JSDOM probably normalized this again, so should really get #14 in place.

out of order / concurrent rendering

Type of Change

  • New Feature Request

Summary

As detailed in , marking visitor pattern functions as async was leading to intermittent failures in the compiler due to nested components not being defined in time before the parent was serialized.

However, it would be nice to solve this!

Details

Definitely more than just marking them as async again, but I could see at very least being able to parallelize siblings? So for instance, something like this

<x-header></x-header>
<x-footer></x-footer>

Should be able to support parallel rendering paths. This might require an initial reading of the DOM but could definitely make things faster where possible.

Literal `this` Evaluation and Derivative References

Type of Change

  • New Feature Request

Summary

Coming from #84 , wanted to track support for having something like this

render() {  
  return (
    <h1>You have {this.todos.length} TODOs left to complete</h1>
  )
} 

Details

I think I was getting this error though when I tried

ReferenceError: __this__ is not defined

I think this would also handle other cases like this, but will need to double check

render() {
  const { user } = this;
  
  return (
    <button onclick={(e) => { this.deleteUser(user.id) }}>Delete User</button>
  )
}

Also would be good handle derivative this references as well, but also make sure we don't track them for observability either. Just make sure we are looking for this in all possible places, e.g.

render() {
  const { count, predicate } = this;
  const conditionClass = predicate ? 'met' : 'unmet';

  return (
    <span class={conditionClass}>{count}</span>
  )
}

Compile JSX to custom element (proof of concept)

Discussed in #69

Originally posted by thescientist13 June 26, 2022
In thinking about a different approach, what if #66 was explored through JSX? 🤔

JSX is similar to tagged template literals, but per my understanding, doesn't prescribe a specific implementation. So perhaps this could be used to provide an (optional) development experience, through a render function, that can take JSX and the compiler can unwind that into valid custom element lifecycles. So no VDOM here, it would just be vanilla JS.

# input
class Counter extends HTMLElement {
  constructor() {
    this.count = 0;
  }

  dec() {
    this.count -= 1;
  }
  
  inc() {
    this.count += 1;
  }

  render() {
    const count = this;
   
    return (
      <button click={this.dec}>-</span>
      <span>{count}</span>
      <button click={this.inc}>+</span>      
    )
  }
}
# output
class Counter extends HTMLElement {
  constructor() {
    this.count = 0;
  }

  static get observedAttributes() { 
    return ['count'];
  }

  set count() {
    this.render();
  }
  
  dec() {
    this.count -= 1;
  }
  
  inc() {
    this.count += 1;
  }

  render() {
    const count = this;
   
    return `
      <button click=${this.dec}>-</span>
      <span>${count}</span>
      <button click=${this.inc}>+</span>      
    `;
  }
}

Or something like that, the devil is in details and so will want to play around with it, but think I can probably get a prototype whipped up.

Some useful links / references:

Perhaps this is something built into #29 ?

expand (no-op) support of DOM shim for DOM traversal methods

Type of Change

  • New Feature Request

Summary

Tracking a number of items that I think the DOM shim should be able to support, though I'm not sure how feasible / realistic any of them are, so will need to do some discovery of what makes sense to fall "in bounds" for SSR, short of just becoming a headless browser.

Details

Not sure how many of these can / should be supported, but some of them at least, if not at least for no-ops

  • parentNodes
  • querySelector / querySelectorAll
  • getElementBy[Id|ClassName|TagName|...]
  • Node <> Element <> HTMLElement (ensure proper inheritance hierarchy)

Questions

  • Is appendNode implemented correctly?
  • Use ASTs in DOM shim (?)

Fine Grained Inferred Observability for JSX

Type of Change

Feature

Summary

Building off the outcome of #87 , would like to, and have already started playing around with, a more "fine grained" observability model; one that doesn't require an entire re-render and blowing out innerHTML, but can instead more acutely update DOM nodes (textContent, setAttribute) instead.

Details

So for example, taking our Counter component

export default class Counter extends HTMLElement {
  ...

  render() {
    const { count } = this;

    return (
      <div>
        <button onclick={this.count -= 1}> -</button>
        <span>You have clicked <span class="red">{count}</span> times</span>
        <button onclick={this.count += 1}> +</button>
      </div>
    );
  }
}

Which would produce this compiled output

export default class Counter extends HTMLElement {
  static get observedAttributes() {
      return ['count'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    function getValue(value) {
      return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value;
    }
    if (newValue !== oldValue) {
      switch (name) {
        case 'count':
          this.count = getValue(newValue);
          break;
        }
        this.render();
      }
  }
 ...
}

Instead, we would want the compiled output to look something like this instead

export default class Counter extends HTMLElement {
  static get observedAttributes() {
      return ['count'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    function getValue(value) {
      return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value;
    }
    if (newValue !== oldValue) {
      switch (name) {
        case 'count':
          this.count = getValue(newValue);
          break;
        }
       this.update(name, oldValue, newValue);
      }
  }

  update(name, oldValue, newValue) {
    const attr = \`data-wcc-\${name}\`;
    const selector = \`[\${attr}]\`;

    this.querySelectorAll(selector).forEach((el) => {
      const needle = oldValue || el.getAttribute(attr);
      switch(el.getAttribute('data-wcc-ins')) {
        case 'text':
          el.textContent = el.textContent.replace(needle, newValue);
          break;
        case 'attr':
          if (el.hasAttribute(el.getAttribute(attr))) {
            el.setAttribute(el.getAttribute(attr), newValue);
          }
          break;
      }
    })
  }
 ...
}

Additional Thoughts:

  • It might stand to reason we should map updates back to attributes, to keep things in sync? But not sure if this co-mingling is good or bad? Probably if state is meant to go "out" it should be done through custom events instead? Will need to play around with this a bit
  • Would be good to explore tagged template functions as part of this, if not for at least the underlying templating mechanics (as opposed to instead of using JSX directly)
  • Although we only scan the render function for this references, would we do ourselves a service by scanning constructor too, maybe for init values and / or something related to SSR?
  • Not sure if dataset could be useful for anything?

deprecate `lightMode` configuration option

Type of Change

  • Other (please clarify below)

Summary

Per #49 / #50 , it seems that there was a much easier solution to the problem of wanting static content in the Light DOM all along; just use innerHTML 😅

Details

I think given how using innerHTML is just standard DOM and per #50 allows each component to opt-in individually, it seems to make this configuration option irrelevant, for now at least. So should

  1. Refactor website to use innerHTML
  2. Update compiler to drop support for lightMode configuration and remove from build.js
  3. Update documentation to remove reference to lightMode
  4. Open up a discussion to capture the original intent / use case, just for posterity in case its something worth re-investigating
  5. Update README
  6. Not sure if this implicates this getInnerHTML issue at all?

leverage `getRootNode()` for referencing Shadow DOM based parent node reference in JSX output

Type of Change

  • Enhancement

Summary

So stumbled upon getRootNode() in MDN and this seems this may allow us to make the output of our serialization process a little more compact when handling this references to hosts (components) when calling event handlers or otherwise referencing state that lives at the Custom Element (definition) class level.

⚠️ It should be noted that this will only "work" with Light DOM contents, as without a Shadow Root, getRootNode() will go all the way to the top level document of the page, not the custom element tag.

Details

For example, WCC's JSX let's you write something like this

export default class Counter extends HTMLElement {
  ...
 
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
    }
  }
  
  render() {
    const { count } = this;

    return (
      <div>
        <button onclick={this.count -= 1}> -</button>
        <span>You have clicked <span class="red">{count}</span> times</span>
        <button onclick={this.count += 1}> +</button>
      </div>
    );
  }
}

customElements.define('wcc-counter', Counter)

Which because of how this works, means you would be referencing increment on the <button>, not <wcc-counter>. So get that correct chain of references to the host element (<wcc-counter>) WCC / JSX recursively walks the HTML structure to come up with something like this

export default class Counter extends HTMLElement {
  ...
  
  render() {
      const {count} = this;
      this.shadowRoot.innerHTML = `<div>
        <button onclick="this.parentElement.parentElement.count-=1; this.parentElement.parentElement.render();"> -</button>
        <span>You have clicked ${ undefined.count } times !!!</span>
        <button onclick="this.parentElement.parentElement.count+=1; this.parentElement.parentElement.render();"> +</button>
      </div>`;
  }
}
customElements.define('wcc-counter', Counter);

BUT, it seems like with getRootNode, we could sidestep all that and can just reference this.getRootNode() directly?

export default class Counter extends HTMLElement {
  ...
  
  render() {
      const {count} = this;
      this.shadowRoot.innerHTML = `<div>
        <button onclick="this.getRootNode().count-=1; this.getRootNode().render();"> -</button>
        <span>You have clicked ${count} times !!!</span>
        <button onclick="this.getRootNode().count+=1; this.getRootNode().render();"> +</button>
      </div>`;
  }
}
customElements.define('wcc-counter', Counter);

Presumably this only works with a Shadow Root, as initial testing seemed to indicate that in a Light DOM context, the "root" node was actually the document, or something like that. So should play around with this first in the browser a bit at least just to get a feel for it.

refactor custom element definitions being shared

Type of Change

  • Other (please clarify below)

Summary

As encountered in #21 , it was determined through mocha testing that having definitions outside of any function, would effectively act as a global variable between runs, or otherwise cause caching issues in NodeJS between renderToString calls.

Details

I had always wanted a better way to code this initially as definitions is updated / read globally by all those functions, so is effectively global state, which is not idea. And as revealed in that PR and verified in mochajs/mocha#1637, this causes odd behaviors.

So basically we need to get rid of let definitions as a shared variable, and still somehow make it all work. Also open to using better data structures if it fits, like a Map or Set.

Also seeing an issue in Greenwood when using metadata for hydration it was breaking GitHub Actions on Windows. 😬

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.