Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 21 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,9 @@

> _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢

## Overview
## How It Works

**Web Components Compiler (WCC)** is a NodeJS package designed to make server-side rendering (SSR) of native Web Components easier. It can render (within reason 😅) your Web Component into static HTML leveraging [Declarative Shadow DOM](https://web.dev/declarative-shadow-dom/).

It is not a static site generator or framework. It is focused on producing raw HTML from Web Components with the intent of being easily _integrated_ into a site generator or framework.

> _The original motivation for this project was to create a [purpose built, lighter weight alternative to puppeteer for SSR of native `HTMLElement` based Web Components](https://github.com/ProjectEvergreen/greenwood/issues/935) for the project [**Greenwood**](https://www.greenwoodjs.io/)._

In addition, WCC hopes to provide a surface area to explore patterns around [streaming](https://github.com/ProjectEvergreen/wcc/issues/5) and serverless rendering, as well as acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg) discussions around community protocols, like [hydration](https://github.com/ProjectEvergreen/wcc/issues/3).

## Key Features

1. Supports the following `HTMLElement` lifecycles and methods on the server side
- `constructor`
- `connectedCallback`
- `attachShadow`
- `innerHTML`
- `[get|set|has]Attribute`
1. Recursive rendering of nested custom elements
1. Optional Declarative Shadow DOM (for producing purely content driven static pages)
1. Metadata and runtime hints to support progressive hydration and lazy loading strategies

## Installation

**wcc** can be installed from npm.

```shell
$ npm install wc-compiler --save-dev
```

## Usage

WCC exposes a few utilities to render your Web Components. Below is one example, with [full docs and more examples](https://merry-caramel-524e61.netlify.app/) available on the website.

1. Given a custom element like so:
1. Write a Web Component
```js
const template = document.createElement('template');

Expand Down Expand Up @@ -68,17 +36,13 @@ WCC exposes a few utilities to render your Web Components. Below is one example

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

1. Using NodeJS, create a file that imports `renderToString` and provide it the path to your web component
1. Run it through the compiler
```js
import { renderToString } from 'wc-compiler';

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

console.debug({ html })
const { html } = await renderToString(new URL('./path/to/component.js', import.meta.url));
```

1. You will get the following HTML output that can be used in conjunction with your preferred site framework or templating solution.
1. Get HTML!
```html
<wcc-footer>
<template shadowroot="open">
Expand All @@ -96,5 +60,20 @@ WCC exposes a few utilities to render your Web Components. Below is one example
</wcc-footer>
```

## Installation

**WCC** runs on NodeJS and can be installed from npm.

```shell
$ npm install wc-compiler --save-dev
```

## Documentation

See our [website](https://merry-caramel-524e61.netlify.app/) 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**](https://github.com/ProjectEvergreen/greenwood/), the original motivation for creating [this project](https://github.com/ProjectEvergreen/greenwood/issues/935).

> _**Make sure to test in Chrome, or other Declarative Shadow DOM compatible browser, otherwise you will need to include the [DSD polyfill](https://web.dev/declarative-shadow-dom/#polyfill).**_
In addition, **WCC** hopes to provide a surface area to explore patterns around [streaming](https://github.com/ProjectEvergreen/wcc/issues/5) and serverless rendering, as well as acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg) discussions around community protocols, like [hydration](https://github.com/ProjectEvergreen/wcc/issues/3).
25 changes: 11 additions & 14 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import fs from 'node:fs/promises';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrism from '@mapbox/rehype-prism';
import rehypeSlug from 'rehype-slug';
import rehypeStringify from 'rehype-stringify';
import rehypeRaw from 'rehype-raw';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkToc from 'remark-toc';
import { unified } from 'unified';

import { renderToString } from './src/wcc.js';
Expand All @@ -12,7 +15,7 @@ async function init() {
const distRoot = './dist';
const pagesRoot = './docs/pages';
const pages = await fs.readdir(new URL(pagesRoot, import.meta.url));
const { html } = await renderToString(new URL('./docs/index.js', import.meta.url), {
const { html } = await renderToString(new URL('./docs/layout.js', import.meta.url), {
lightMode: true
});

Expand All @@ -26,26 +29,20 @@ async function init() {
await fs.copyFile(new URL('./docs/assets/favicon.ico', import.meta.url), new URL(`${distRoot}/favicon.ico`, import.meta.url));

for (const page of pages) {
// for now, just repurposing the README for home page content
const isHomePage = page === 'index.md';
const pageLocation = isHomePage ? './README.md' : `${pagesRoot}/${page}`;
const markdown = await fs.readFile(new URL(pageLocation, import.meta.url), 'utf-8');
let content = (await unified()
const route = page.replace('.md', '');
const outputPath = route === 'index' ? '' : `${route}/`;
const markdown = await fs.readFile(new URL(`${pagesRoot}/${page}`, import.meta.url), 'utf-8');
const content = (await unified()
.use(remarkParse)
.use(remarkToc, { tight: true })
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeSlug)
.use(rehypeRaw)
.use(rehypeAutolinkHeadings)
.use(rehypePrism)
.use(rehypeStringify)
.process(markdown)).value;

if (isHomePage) {
const contentFilter = content.substring(content.indexOf('<h1>wcc</h1>'), content.indexOf('<h2>Overview</h2>') + 17);
content = content.replace(contentFilter, '');
}

const route = page.replace('.md', '');
const outputPath = route === 'index' ? '' : `${route}/`;

await fs.mkdir(`./dist/${outputPath}`, { recursive: true });
await fs.mkdir(`${distRoot}/${outputPath}`, { recursive: true });

Expand Down
5 changes: 3 additions & 2 deletions docs/components/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ template.innerHTML = `
list-style-type: none;
overflow: auto;
grid-column: 1 / -1;
width: 90%;
}

nav ul li {
float: left;
width: 33%;
width: 33.3%;
text-align: center;
}

nav ul li a, nav ul li a:visited {
display: inline-block;
color: #efefef;
min-width: 48px;
min-height: 48px;
font-size: 2.5rem;
}
</style>

Expand Down
4 changes: 2 additions & 2 deletions docs/index.js → docs/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ template.innerHTML = `
<wcc-footer></wcc-footer>
`;

class Home extends HTMLElement {
class Layout extends HTMLElement {

connectedCallback() {
if (!this.shadowRoot) {
Expand All @@ -45,4 +45,4 @@ class Home extends HTMLElement {
}
}

export default Home;
export default Layout;
24 changes: 13 additions & 11 deletions docs/pages/docs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Documentation

## Table of contents

## API

### renderToString
Expand Down Expand Up @@ -46,11 +48,11 @@ class Home extends HTMLElement {
export default Home;
```

> _**Note**: `wcc` will decide to wrap or not wrap your entry point's HTML in a custom element tag if you do or do not, respectively, have a `customElements.define` statement in your entry point. `wcc` will use the tag name you define as the custom element tag name in the HTML._
> _**Note**: **WCC** will wrap or not wrap your _entry point's HTML_ in a custom element tag if you do or do not, respectively, include a `customElements.define` in your entry point. **WCC** will use the tag name you define as the custom element tag name in the generated HTML._

### renderFromHTML

This function takes a string of HTML and an array of any top-level custom elements used with `import`, and returns the static HTML output of the rendered content.
This function takes a string of HTML and an array of any top-level custom elements used in the HTML, and returns the static HTML output of the rendered content.

```js
const { html } = await renderFromHTML(`
Expand All @@ -64,13 +66,15 @@ const { html } = await renderFromHTML(`
<wcc-footer></wcc-footer>
</body>
</html>
`,
`,
[
new URL('./src/components/footer.js', import.meta.url),
new URL('./src/components/header.js', import.meta.url)
]);
```

For example, even if `Header` or `Footer` use `import` to pull in additional custom elements, only the `Header` and `Footer custom elements used in the "entry" HTML are needed in the array.

### Options

`renderToString` and `renderFromHTML` also supports a second and third parameter respectively, that is an object, called `options`
Expand All @@ -88,7 +92,7 @@ It supports the following configuration(s):

## Metadata

`renderToString` and `renderFromHTML` return not only HTML, but also metadata about all the custom elements registered as part of rendering the top level custom element.
`renderToString` and `renderFromHTML` return not only HTML, but also metadata about all the custom elements registered as part of rendering the entry file.

```js
const { metadata } = await renderToString(new URL('./src/index.js', import.meta.url));
Expand All @@ -102,7 +106,7 @@ console.log({ metadata });
* 'wcc-navigation': { instanceName: 'Navigation', moduleURL: [URL] }
* ]
* }
*
*
```

## Progressive Hydration
Expand All @@ -112,7 +116,7 @@ To achieve an islands architecture implementation, if you add `hydration="true"`
<wcc-footer hydration="true"></wcc-footer>
```

This will be reflected in the returned `metadata` array from `renderToString`.
This will be reflected in the returned `metadata` array from `renderToString`.
```js
/*
* {
Expand All @@ -122,12 +126,12 @@ This will be reflected in the returned `metadata` array from `renderToString`.
* 'wcc-navigation': { instanceName: 'Navigation', moduleURL: [URL] }
* ]
* }
*
*
```

The benefit is that this hint can be used to defer loading of these scripts by using an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) (for example), instead of eagerly loading it on page load using a `<script>` tag.

> _See our [examples page](/examples/) for more info._
> _See [this example](/examples/#progressive-hydration) for more information._


## Data
Expand All @@ -143,10 +147,8 @@ export async function getData() {
}
```

> _See our [examples page](/examples/) for more info._

## Conventions

- Make sure to define your custom elements with `customElements.define`
- Make sure to `export default` your custom element base class
- Make sure to include a `export default` for your custom element base class
- Avoid [touching the DOM in `constructor` methods](https://twitter.com/techytacos/status/1514029967981494280)
Loading