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
1 change: 0 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
module.exports = {
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ jobs:
yarn test --parallel
- name: Build
run: |
yarn clean && yarn ssg
yarn clean && yarn build
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ jobs:
yarn test --parallel
- name: Build
run: |
yarn clean && yarn ssg
yarn clean && yarn build
28 changes: 28 additions & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Master Integration

on:
push:
branches:
- master

jobs:

build:
runs-on: ubuntu-18.04

strategy:
matrix:
node: [16]

steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Installing project dependencies
run: |
yarn install --frozen-lockfile
- name: Build
run: |
yarn clean && yarn build
96 changes: 94 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,101 @@
# wcc

[![Netlify Status](https://api.netlify.com/api/v1/badges/e718eac2-b3bc-4986-8569-49706a430beb/deploy-status)](https://app.netlify.com/sites/merry-caramel-524e61/deploys)
[![GitHub release](https://img.shields.io/github/tag/thescientist13/wcc.svg)](https://github.com/thescientist13/wcc/tags)
![GitHub Actions status](https://github.com/thescientist13/wcc/workflows/Master%20Integration/badge.svg)
[![GitHub issues](https://img.shields.io/github/issues-pr-raw/thescientist13/wcc.svg)](https://github.com/thescientist13/wcc/issues)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/thescientist13/wcc/master/LICENSE.md)

Experimental native Web Components compiler. (`<w⚙️⚙️/>`)

> _It's Web Components all the way down._ 🐢

## Overview

It's Web Components all the way down. 🐢
**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 `HTMLElement`](https://github.com/ProjectEvergreen/greenwood/issues/926) 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/thescientist13/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/thescientist13/wcc/issues/3).

## Key Features

1. Supports the following `HTMLElement` lifecycles and methods on the server side
- `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

TODO

## Usage

WCC exposes a few utilities to render your Web Components. Below is one example, with [full docs and more examples](https://wcc.greenwoodjs.io) available on the website.

1. Given a custom element like so:
```js
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);
```

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

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

console.debug({ html })
```

1. You will get the following html output that can be used in conjunction with your preferred site framework or templating solution.
```html
<wcc-footer>
<template shadowroot="open">
<style>
.footer {
color: white;
background-color: #192a27;
}
</style>

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


> _**Make sure to test in Chrome, or other Declarative Shadow DOM compatible browser.**_
> _**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).**_
3 changes: 0 additions & 3 deletions babel.config.cjs

This file was deleted.

84 changes: 84 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from 'node:fs/promises';
import rehypeStringify from 'rehype-stringify';
import rehypeRaw from 'rehype-raw';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { unified } from 'unified';

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

async function init() {
const distRoot = './dist';
const pagesRoot = './docs/pages';
const pages = await fs.readdir(new URL(pagesRoot, import.meta.url));

// await fs.rm(distRoot, { recursive: true, force: true });
// await fs.mkdir('./dist', { recursive: true });
// await fse.copy('./www/assets', `${distRoot}/www/assets`);
// await fse.copy('./www/components', `${distRoot}/www/components`);
// await fse.copy('./docs/pages', `${distRoot}/www/pages`);

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()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.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 { html } = await renderToString(new URL('./docs/index.js', import.meta.url), false);

// const lazyJs = [];
// const eagerJs = [];

// for (const asset in assets) {
// const a = assets[asset];

// a.tagName = asset;

// if (a.moduleURL.href.endsWith('.js')) {
// if (a.hydrate === 'lazy') {
// lazyJs.push(a);
// } else {
// eagerJs.push(a);
// }
// }
// }

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

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

await fs.writeFile(new URL(`${distRoot}/${outputPath}/index.html`, import.meta.url), `
<!DOCTYPE html>
<html lang="en" prefix="og:http://ogp.me/ns#">

<head>
<title>Web Components Compiler (WCC)</title>
<meta property="og:title" content="Web Components Compiler (WCC)"/>
<link rel="stylesheet" href="https://unpkg.com/simpledotcss@2.1.0/simple.min.css">
</head>

<body>

${html.replace('<slot name="content"></slot>', content)}

</body>
</html>
`.trim());
}
}

init();
47 changes: 47 additions & 0 deletions docs/components/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const template = document.createElement('template');

template.innerHTML = `
<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 &#9672 Project Evergreen</a>
</h4>
</footer>
`;

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

export {
Footer
};

customElements.define('wcc-footer', Footer);
59 changes: 59 additions & 0 deletions docs/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import './navigation.js';

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

template.innerHTML = `
<style>
header {
background-color: var(--accent);
grid-column: 1 / -1;
}

header .social {
text-align: right;
padding: 10px 10px 0 0;
}

header img.github-badge {
float: right;
display: inline-block;
padding: 10px;
align-items: top;
width: 90px;
height: 20px;
}
</style>

<header>
<div>
<a href="/">
<img src="/www/assets/wcc-logo.jpg" alt="WCC logo" class="logo"/>
</a>

<a href="https://github.com/ProjectEvergreen/wcc" class="social">
<img
src="https://img.shields.io/github/stars/ProjectEvergreen/wcc.svg?style=social&logo=github&label=github"
alt="WCC GitHub badge"
class="github-badge"
/>
</a>
</div>

<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);
Loading