├── .c8rc.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-feature-request.yml │ ├── 2-enhancement.yml │ ├── 3-bug-report.yml │ ├── 4-chore.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci-jsx-win.yml │ ├── ci-jsx.yml │ ├── ci-win.yml │ └── ci.yml ├── .gitignore ├── .ls-lint.yml ├── .npmrc ├── .nvmrc ├── CONTRIBUTING.md ├── README.md ├── build.js ├── docs ├── assets │ ├── favicon.ico │ └── wcc-logo.png ├── components │ ├── footer.js │ ├── header.js │ └── navigation.js ├── layout.js └── pages │ ├── docs.md │ ├── examples.md │ └── index.md ├── eslint.config.js ├── netlify.toml ├── package-lock.json ├── package.json ├── sandbox.js ├── sandbox ├── components │ ├── card.js │ ├── card.jsx │ ├── counter-dsd.jsx │ ├── counter.jsx │ ├── greeting.ts │ ├── header.js │ ├── header.jsx │ └── picture-frame.js └── index.html ├── src ├── dom-shim.js ├── index.d.ts ├── jsx-loader.js ├── register.js └── wcc.js ├── test-loader.js ├── test-register.js ├── test └── cases │ ├── attributes │ ├── attributes.spec.js │ └── src │ │ ├── components │ │ └── counter.js │ │ └── index.js │ ├── children-and-slots │ ├── children-and-slots.spec.js │ └── src │ │ ├── components │ │ └── paragraph.js │ │ └── pages │ │ └── index.js │ ├── constructable-stylesheet │ ├── constructabe-stylesheet.spec.js │ └── src │ │ ├── components │ │ └── header │ │ │ └── header.js │ │ └── pages │ │ └── index.js │ ├── constructor-props │ ├── constructor-props.spec.js │ └── src │ │ └── index.js │ ├── create-document-fragment │ ├── create-document-fragment.spec.js │ └── src │ │ └── index.js │ ├── custom-extension │ ├── custom-extension.spec.js │ └── src │ │ ├── banner.css │ │ ├── banner.js │ │ └── footer.js │ ├── element-props │ ├── element-props.spec.js │ └── src │ │ ├── components │ │ ├── prop-passer.js │ │ └── prop-receiver.js │ │ ├── index.js │ │ └── renderer.js │ ├── empty │ ├── empty.spec.js │ └── src │ │ └── empty.js │ ├── event-listener │ ├── event-listener.spec.js │ └── src │ │ └── my-component.js │ ├── full-document-component │ ├── full-document-component.spec.js │ └── src │ │ └── index.js │ ├── get-data │ ├── get-data.spec.js │ └── src │ │ ├── components │ │ └── counter.js │ │ └── index.js │ ├── html-web-components │ ├── expected.html │ ├── html-web-components.spec.js │ └── src │ │ ├── components │ │ ├── caption.js │ │ └── picture-frame.js │ │ └── pages │ │ └── index.js │ ├── import-attributes │ ├── import-attributes.spec.js │ └── src │ │ ├── components │ │ └── header │ │ │ ├── data.json │ │ │ └── header.js │ │ └── pages │ │ └── index.js │ ├── jsx-inferred-observability │ ├── fixtures │ │ ├── attribute-changed-callback.txt │ │ └── get-observed-attributes.txt │ ├── jsx-inferred-obsevability.spec.js │ └── src │ │ └── counter.jsx │ ├── jsx-shadow-dom │ ├── jsx-shadow-dom.spec.js │ └── src │ │ └── heading.jsx │ ├── jsx │ ├── jsx.spec.js │ └── src │ │ ├── badge.jsx │ │ └── counter.jsx │ ├── light-dom │ ├── light-dom.spec.js │ └── src │ │ ├── components │ │ ├── header.js │ │ └── navigation.js │ │ └── pages │ │ └── index.js │ ├── metadata │ ├── metadata.spec.js │ └── src │ │ ├── components │ │ ├── footer.js │ │ ├── header.js │ │ └── navigation.js │ │ └── pages │ │ └── index.js │ ├── nested-elements │ ├── nested-elements.spec.js │ └── src │ │ ├── assets │ │ └── navigation.js │ │ ├── components │ │ ├── footer.js │ │ └── header.js │ │ └── pages │ │ └── index.js │ ├── no-define │ ├── no-define.spec.js │ └── src │ │ ├── footer.js │ │ ├── header.js │ │ └── no-define.js │ ├── no-export │ ├── no-export.spec.js │ └── src │ │ ├── footer.js │ │ ├── header.js │ │ └── no-export.js │ ├── no-wrapping-entry-tag │ ├── no-wrapping-entry-tag.spec.js │ └── src │ │ └── no-wrap.js │ ├── node-modules │ ├── node-modules.spec.js │ └── src │ │ ├── components │ │ └── events-list.js │ │ └── index.js │ ├── render-from-html │ ├── render-from-html.spec.js │ └── src │ │ └── components │ │ ├── header.js │ │ └── navigation.js │ ├── serializable-shadow-roots │ ├── serializable-shadow-roots.spec.js │ └── src │ │ ├── components │ │ ├── serializable-non-ssr-component.js │ │ ├── serializable-ssr-component.js │ │ ├── unserializable-non-ssr-component.js │ │ └── unserializable-ssr-component.js │ │ └── index.js │ ├── set-attribute │ ├── set-attribute.spec.js │ └── src │ │ └── index.js │ ├── shadowrootmode │ ├── shadowrootmode.spec.js │ └── src │ │ ├── components │ │ ├── closed-shadow-component.js │ │ └── open-shadow-component.js │ │ └── index.js │ ├── single-element │ ├── single-element.spec.js │ └── src │ │ └── footer.js │ └── ts │ ├── src │ ├── app.ts │ └── greeting.ts │ └── ts.spec.js └── tsconfig.json /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | 4 | "include": [ 5 | "src/**/**/*.js" 6 | ], 7 | 8 | "reporter": [ 9 | "cobertura", 10 | "html", 11 | "text", 12 | "text-summary" 13 | ], 14 | 15 | "checkCoverage": true, 16 | 17 | "statements": 90, 18 | "branches": 85, 19 | "functions": 85, 20 | "lines": 90, 21 | 22 | "watermarks": { 23 | "statements": [75, 85], 24 | "branches": [75, 85], 25 | "functions": [75, 85], 26 | "lines": [75, 85] 27 | } 28 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.sh text eol=lf 5 | 6 | *.jpg binary 7 | *.jpeg binary 8 | *.png binary 9 | *.avif binary 10 | *.webp binary 11 | *.gif binary 12 | *.ico binary 13 | *.woff binary 14 | *.woff2 binary 15 | *.ttf binary 16 | *.eot binary -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Submit a new feature request for WCC. 3 | title: "Support x in WCC" 4 | labels: ["feature"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for submitting a feature request to WCC! 10 | - type: textarea 11 | id: summary 12 | attributes: 13 | label: Motivation 14 | description: Please provide a summary of the feature you are requesting and use case(s). 15 | placeholder: It would be great if WCC could do... 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: Motivation 20 | attributes: 21 | label: Technical Design 22 | description: Detail usage within a WCC project and the API design you have in mind. 23 | placeholder: I want to use the feature like this... 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: additional-context 28 | attributes: 29 | label: Additional Context 30 | description: Please provide any additional details, links, or resources that may be helpful for consideration of this feature. 31 | validations: 32 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-enhancement.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Enhancement 2 | description: Improve an existing feature in WCC 3 | title: "Refactor Feature X to use..." 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for wanting to improve WCC! 10 | - type: textarea 11 | id: current-state 12 | attributes: 13 | label: Current State 14 | description: Please provide a summary of the current state of a feature or capability of WCC. 15 | placeholder: Currently feature x works like this... 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: desired-state 20 | attributes: 21 | label: Desired State 22 | description: Describe what the desired state could look like. 23 | placeholder: Feature x could work like this instead... 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: additional-context 28 | attributes: 29 | label: Additional Context 30 | description: Please provide any additional details, links, or resources that may be helpful for consideration of this enhancement. 31 | validations: 32 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report. 3 | title: "Description of the issue" 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Please provide a summary of the issue or error you are seeing. 15 | placeholder: Tell us what you see! 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: reproduction-steps 20 | attributes: 21 | label: Steps to reproduce 22 | description: Please provide a set of steps to reproduce the issue and ideally a link to a minimal reproduction. 23 | placeholder: | 24 | 1. I installed x 25 | 2. I added y 26 | 3. I ran z 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: environment 31 | attributes: 32 | label: Environment 33 | description: Please provide details of the environment your running in. 34 | placeholder: WCC version, operating system, runtime version, browser, etc 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: additional-context 39 | attributes: 40 | label: Additional Context 41 | description: Please provide any additional relevant details, screenshots, links, or resources that would be helpful. 42 | validations: 43 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-chore.yml: -------------------------------------------------------------------------------- 1 | name: ⚙️ Chore 2 | description: Technical tasks not related to WCC 3 | title: "Update repo configuration, maintaining dependencies, etc" 4 | labels: ["chore"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for helping out! 10 | - type: textarea 11 | id: task 12 | attributes: 13 | label: Task 14 | description: Description of the task at hand 15 | placeholder: Update ESLint config, change test runner, etc... 16 | validations: 17 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: GitHub Discussions 5 | url: https://github.com/ProjectEvergreen/wcc/discussions 6 | about: Please ask and answer questions here or discuss new ideas for the project. 7 | - name: Discord 8 | url: https://www.greenwoodjs.dev/discord/ 9 | about: Chat about Greenwood and join the community! -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Related Issue 7 | 8 | 9 | ## Summary of Changes 10 | -------------------------------------------------------------------------------- /.github/workflows/ci-jsx-win.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Windows (Experimental) 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: windows-latest 9 | 10 | strategy: 11 | matrix: 12 | node: [22] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - name: Installing project dependencies 21 | run: | 22 | npm ci 23 | - name: Lint 24 | run: | 25 | npm run lint 26 | - name: Test 27 | run: | 28 | npm run test:jsx 29 | - name: Build 30 | run: | 31 | npm run docs:build -------------------------------------------------------------------------------- /.github/workflows/ci-jsx.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration (Experimental) 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node: [22] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - name: Installing project dependencies 21 | run: | 22 | npm ci 23 | - name: Lint 24 | run: | 25 | npm run lint 26 | - name: Test 27 | run: | 28 | npm run test:jsx 29 | - name: Build 30 | run: | 31 | npm run docs:build -------------------------------------------------------------------------------- /.github/workflows/ci-win.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Windows 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: windows-latest 9 | 10 | strategy: 11 | matrix: 12 | node: [18, 20, 22] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - name: Installing project dependencies 21 | run: | 22 | npm ci 23 | - name: Lint 24 | run: | 25 | npm run lint 26 | - name: Test 27 | run: | 28 | npm test 29 | - name: Build 30 | run: | 31 | npm run docs:build -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node: [18, 20, 22] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - name: Installing project dependencies 21 | run: | 22 | npm ci 23 | - name: Lint 24 | run: | 25 | npm run lint 26 | - name: Check Types 27 | run: | 28 | npm run lint:types 29 | - name: Test 30 | run: | 31 | npm test 32 | - name: Build 33 | run: | 34 | npm run docs:build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vscode/ 3 | coverage/ 4 | dist/ 5 | node_modules/ -------------------------------------------------------------------------------- /.ls-lint.yml: -------------------------------------------------------------------------------- 1 | ls: 2 | .spec.js: kebabcase 3 | .config.js: kebabcase 4 | .js: kebabcase 5 | .*.json: kebabcase 6 | .css: kebabcase 7 | .html: kebabcase 8 | .ico: kebabcase 9 | .jpg: kebabcase 10 | .png: kebabcase 11 | .svg: kebabcase 12 | .tff: kebabcase 13 | .woff: kebabcase 14 | .woff2: kebabcase 15 | 16 | ignore: 17 | - .git 18 | - coverage 19 | - node_modules 20 | - dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Welcome! 4 | 5 | We're excited for your interest in WCC, and maybe even your contribution! 6 | 7 | ## Setup 8 | 9 | To develop for the project, you'll want to follow these steps: 10 | 11 | 1. Have [NodeJS LTS](https://nodejs.org) and / or use `nvm` (see below) 12 | 1. Clone the repository 13 | 1. Run `npm ci` 14 | 15 | ### NVM 16 | If you have **NVM (Node Version Manager)** installed, get the recommend node version: 17 | 18 | - Windows: [NVM for Windows](https://github.com/coreybutler/nvm-windows/releases) 19 | - Linux/MacOS: [Node Version Manager](https://github.com/nvm-sh/nvm) 20 | 21 | And then running `nvm use` 22 | 23 | ```sh 24 | $ nvm use 25 | ``` 26 | 27 | ## Local Development 28 | 29 | The local development flow is based around building the docs website, using `wcc` in an SSG based workflow, and running tests. 30 | 31 | ### Commands 32 | 33 | There are the main tasks, but you can see them all listed in _package.json#scripts_. 34 | 35 | - `npm run docs:dev` - Builds the docs site for local development 36 | - `npm start` - Builds a production version of the docs site and serves it locally 37 | - `npm run sandbox` - Starts the sandbox app for live demos and testing 38 | - `npm test` - Run all the tests 39 | - `npm test:tdd` - Run all the tests in watch mode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Web Components Compiler (WCC) 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/e718eac2-b3bc-4986-8569-49706a430beb/deploy-status)](https://app.netlify.com/sites/merry-caramel-524e61/deploys) 6 | [![GitHub release](https://img.shields.io/github/tag/ProjectEvergreen/wcc.svg)](https://github.com/ProjectEvergreen/wcc/tags) 7 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ProjectEvergreen/wcc/master/LICENSE.md) 8 | [![NodeJS compatibility](https://img.shields.io/node/v/wc-compiler.svg)](https://nodejs.org/en/about/previous-releases") 9 | [![Discord Chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://www.greenwoodjs.dev/discord/) 10 | 11 | > _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢 12 | 13 | ## How It Works 14 | 15 | 1. Write a Web Component 16 | ```js 17 | const template = document.createElement('template'); 18 | 19 | template.innerHTML = ` 20 | 26 | 27 | 30 | `; 31 | 32 | class Footer extends HTMLElement { 33 | connectedCallback() { 34 | if (!this.shadowRoot) { 35 | this.attachShadow({ mode: 'open' }); 36 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 37 | } 38 | } 39 | } 40 | 41 | export default Footer; 42 | 43 | customElements.define('wcc-footer', Footer); 44 | ``` 45 | 1. Run it through the compiler 46 | ```js 47 | import { renderToString } from 'wc-compiler'; 48 | 49 | const { html } = await renderToString(new URL('./path/to/component.js', import.meta.url)); 50 | ``` 51 | 1. Get HTML! 52 | ```html 53 | 54 | 66 | 67 | ``` 68 | 69 | ## Installation 70 | 71 | **WCC** runs on NodeJS and can be installed from npm. 72 | 73 | ```shell 74 | $ npm install wc-compiler --save-dev 75 | ``` 76 | 77 | ## Documentation 78 | 79 | See our [website](https://merry-caramel-524e61.netlify.app/) for API docs and examples. 80 | 81 | ## Motivation 82 | 83 | **WCC** is not a static site generator, framework or bundler. It is designed with the intent of being able to produce raw HTML from standards compliant Web Components and easily integrated _into_ a site generator or framework, like [**Greenwood**](https://www.greenwoodjs.dev). The Project Evergreen team also maintains similar integrations for [**Eleventy**](https://github.com/ProjectEvergreen/eleventy-plugin-wcc/) and [Astro](https://github.com/ProjectEvergreen/astro-wcc). 84 | 85 | In addition, **WCC** hopes to provide a surface area to explore patterns around [streaming](https://github.com/ProjectEvergreen/wcc/issues/5), [serverless and edge rendering](https://github.com/thescientist13/web-components-at-the-edge), and as acting as a test bed for the [Web Components Community Groups](https://github.com/webcomponents-cg)'s discussions around community protocols, like [hydration](https://github.com/ProjectEvergreen/wcc/issues/3). 86 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 3 | import rehypePrism from '@mapbox/rehype-prism'; 4 | import rehypeSlug from 'rehype-slug'; 5 | import rehypeStringify from 'rehype-stringify'; 6 | import rehypeRaw from 'rehype-raw'; 7 | import remarkParse from 'remark-parse'; 8 | import remarkRehype from 'remark-rehype'; 9 | import remarkToc from 'remark-toc'; 10 | import { unified } from 'unified'; 11 | 12 | import { renderToString } from './src/wcc.js'; 13 | 14 | async function init() { 15 | const distRoot = './dist'; 16 | const pagesRoot = './docs/pages'; 17 | const pages = await fs.readdir(new URL(pagesRoot, import.meta.url)); 18 | const { html } = await renderToString(new URL('./docs/layout.js', import.meta.url)); 19 | 20 | await fs.rm(distRoot, { recursive: true, force: true }); 21 | await fs.mkdir(distRoot, { recursive: true }); 22 | await fs.mkdir(`${distRoot}/assets`, { recursive: true }); 23 | 24 | await fs.copyFile(new URL('./node_modules/prismjs/themes/prism.css', import.meta.url), new URL(`${distRoot}/prism.css`, import.meta.url)); 25 | await fs.copyFile(new URL('./node_modules/simple.css/dist/simple.min.css', import.meta.url), new URL(`${distRoot}/simple.min.css`, import.meta.url)); 26 | await fs.cp(new URL('./docs/assets', import.meta.url), new URL(`${distRoot}/assets`, import.meta.url), { recursive: true }); 27 | await fs.copyFile(new URL('./docs/assets/favicon.ico', import.meta.url), new URL(`${distRoot}/favicon.ico`, import.meta.url)); 28 | 29 | for (const page of pages) { 30 | const route = page.replace('.md', ''); 31 | const outputPath = route === 'index' ? '' : `${route}/`; 32 | const markdown = await fs.readFile(new URL(`${pagesRoot}/${page}`, import.meta.url), 'utf-8'); 33 | const content = (await unified() 34 | .use(remarkParse) 35 | .use(remarkToc, { tight: true }) 36 | .use(remarkRehype, { allowDangerousHtml: true }) 37 | .use(rehypeSlug) 38 | .use(rehypeRaw) 39 | .use(rehypeAutolinkHeadings) 40 | .use(rehypePrism) 41 | .use(rehypeStringify) 42 | .process(markdown)).value; 43 | 44 | await fs.mkdir(`./dist/${outputPath}`, { recursive: true }); 45 | await fs.mkdir(`${distRoot}/${outputPath}`, { recursive: true }); 46 | 47 | await fs.writeFile(new URL(`${distRoot}/${outputPath}/index.html`, import.meta.url), ` 48 | 49 | 50 | 51 | 52 | WCC - Web Components Compiler 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ${html.replace('', content)} 69 | 70 | 71 | 72 | `.trim()); 73 | } 74 | } 75 | 76 | init(); -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectEvergreen/wcc/922a0d0ab2198eb3f60ae4cf73a9d2889778a01d/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/wcc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectEvergreen/wcc/922a0d0ab2198eb3f60ae4cf73a9d2889778a01d/docs/assets/wcc-logo.png -------------------------------------------------------------------------------- /docs/components/footer.js: -------------------------------------------------------------------------------- 1 | class Footer extends HTMLElement { 2 | connectedCallback() { 3 | this.innerHTML = this.render(); 4 | } 5 | 6 | render() { 7 | return ` 8 | 27 | 28 | 33 | `; 34 | } 35 | } 36 | 37 | export { 38 | Footer 39 | }; 40 | 41 | customElements.define('wcc-footer', Footer); -------------------------------------------------------------------------------- /docs/components/header.js: -------------------------------------------------------------------------------- 1 | import './navigation.js'; 2 | 3 | class Header extends HTMLElement { 4 | connectedCallback() { 5 | this.innerHTML = this.render(); 6 | } 7 | 8 | render() { 9 | return ` 10 | 43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 59 |
60 | 61 | 62 |
63 |
64 | `; 65 | } 66 | } 67 | 68 | export { 69 | Header 70 | }; 71 | 72 | customElements.define('wcc-header', Header); 73 | -------------------------------------------------------------------------------- /docs/components/navigation.js: -------------------------------------------------------------------------------- 1 | class Navigation extends HTMLElement { 2 | connectedCallback() { 3 | this.innerHTML = this.render(); 4 | } 5 | 6 | render() { 7 | return ` 8 | 29 | 30 | 37 | ` 38 | } 39 | } 40 | 41 | export { 42 | Navigation 43 | }; 44 | 45 | customElements.define('wcc-navigation', Navigation); -------------------------------------------------------------------------------- /docs/layout.js: -------------------------------------------------------------------------------- 1 | import './components/footer.js'; 2 | import './components/header.js'; 3 | 4 | class Layout extends HTMLElement { 5 | connectedCallback() { 6 | this.innerHTML = this.render(); 7 | } 8 | 9 | render() { 10 | return ` 11 | 32 | 33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 | `; 41 | } 42 | } 43 | 44 | export default Layout; -------------------------------------------------------------------------------- /docs/pages/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of contents 4 | 5 | ## API 6 | 7 | ### renderToString 8 | 9 | This function takes a `URL` "entry point" to a JavaScript file that defines a custom element, and returns the static HTML output of its rendered contents. 10 | 11 | 12 | ```js 13 | const { html } = await renderToString(new URL('./src/index.js', import.meta.url)); 14 | ``` 15 | 16 | ```js 17 | // index.js 18 | import './components/footer.js'; 19 | import './components/header.js'; 20 | 21 | const template = document.createElement('template'); 22 | 23 | template.innerHTML = ` 24 | 29 | 30 | 31 | 32 |
33 |

My Blog Post

34 |
35 | 36 | 37 | `; 38 | 39 | class Home extends HTMLElement { 40 | 41 | connectedCallback() { 42 | if (!this.shadowRoot) { 43 | this.attachShadow({ mode: 'open' }); 44 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 45 | } 46 | } 47 | } 48 | 49 | export default Home; 50 | ``` 51 | 52 | You can also manually set `innerHTML` of Shadow Root if you don't want to use a template element 53 | 54 | ```js 55 | // index.js 56 | import './components/footer.js'; 57 | import './components/header.js'; 58 | 59 | class Home extends HTMLElement { 60 | 61 | connectedCallback() { 62 | if (!this.shadowRoot) { 63 | this.attachShadow({ mode: 'open' }); 64 | this.shadowRoot.innerHTML = ` 65 | 70 | 71 | 72 | 73 |
74 |

My Website

75 |
76 | 77 | 78 | `; 79 | } 80 | } 81 | } 82 | 83 | export default Home; 84 | ``` 85 | 86 | > _**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._ 87 | > 88 | > You can opt-out of this by passing `false` as the second parameter to `renderToString`. 89 | > 90 | > ```js 91 | > const { html } = await renderToString(new URL('...'), false); 92 | > ``` 93 | 94 | ### renderFromHTML 95 | 96 | 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. 97 | 98 | 99 | ```js 100 | const { html } = await renderFromHTML(` 101 | 102 | 103 | WCC 104 | 105 | 106 | 107 |

Home Page

108 | 109 | 110 | 111 | `, 112 | [ 113 | new URL('./src/components/footer.js', import.meta.url), 114 | new URL('./src/components/header.js', import.meta.url) 115 | ]); 116 | ``` 117 | 118 | 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. 119 | 120 | ## Metadata 121 | 122 | `renderToString` and `renderFromHTML` return not only HTML, but also metadata about all the custom elements registered as part of rendering the entry file. 123 | 124 | So for the given HTML: 125 | ```html 126 | 127 | 128 |

Hello World

129 | 130 | 131 | ``` 132 | 133 | And the following conditions: 134 | 1. _index.js_ does not define a tag of its own, e.g. using `customElements.define` (e.g. it is just a ["layout" component](/examples/#static-sites-ssg)) 135 | 1. `` imports `` 136 | 137 | The result would be: 138 | ```js 139 | const { metadata } = await renderToString(new URL('./src/index.js', import.meta.url)); 140 | 141 | console.log({ metadata }); 142 | /* 143 | * { 144 | * metadata: { 145 | * 'wcc-footer': { instanceName: 'Footer', moduleURL: [URL], isEntry: true }, 146 | * 'wcc-header': { instanceName: 'Header', moduleURL: [URL], isEntry: true }, 147 | * 'wcc-navigation': { instanceName: 'Navigation', moduleURL: [URL], isEntry: false } 148 | * } 149 | * } 150 | */ 151 | ``` 152 | 153 | ## Progressive Hydration 154 | 155 | To achieve an islands architecture implementation, if you add `hydration="true"` attribute to a custom element, e.g. 156 | ```html 157 | 158 | ``` 159 | 160 | This will be reflected in the returned `metadata` object from `renderToString`. 161 | ```js 162 | /* 163 | * { 164 | * metadata: { 165 | * 'wcc-footer': { instanceName: 'Footer', moduleURL: [URL], hydrate: 'true' }, 166 | * 'wcc-header': { instanceName: 'Header', moduleURL: [URL] }, 167 | * 'wcc-navigation': { instanceName: 'Navigation', moduleURL: [URL] } 168 | * } 169 | * } 170 | * 171 | */ 172 | ``` 173 | 174 | 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 ` 239 | 240 |
241 | 242 | Current Count: ${this.count} 243 | 244 |
245 | `; 246 | } 247 | } 248 | 249 | export async function getData() { 250 | return { 251 | count: Math.floor(Math.random() * (100 - 0 + 1) + 0) 252 | }; 253 | } 254 | ``` 255 | 256 | ## Conventions 257 | 258 | - Make sure to define your custom elements with `customElements.define` 259 | - Make sure to include a `export default` for your custom element base class 260 | - Avoid [touching the DOM in `constructor` methods](https://twitter.com/techytacos/status/1514029967981494280) 261 | 262 | 263 | ## TypeScript 264 | 265 | TypeScript is supported through "type stripping", which is effectively just removing all the TypeScript and leaving only valid JavaScript, before handing off to WCC to do its compiling. 266 | 267 | ```ts 268 | interface User { 269 | name: string; 270 | } 271 | 272 | export default class Greeting extends HTMLElement { 273 | connectedCallback() { 274 | const user: User = { 275 | name: this.getAttribute('name') || 'World' 276 | }; 277 | 278 | this.innerHTML = ` 279 |

Hello ${user.name}! 👋

280 | `; 281 | } 282 | } 283 | 284 | customElements.define('wcc-greeting', Greeting); 285 | ``` 286 | 287 | ### Prerequisites 288 | 289 | There are of couple things you will need to do to use WCC with TypeScript parsing: 290 | 1. NodeJS version needs to be >= `22.6.0` 291 | 1. You will need to use the _.ts_ extension 292 | 1. You'll want to enable the [`erasableSyntaxOnly`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/#the---erasablesyntaxonly-option) flag in your _tsconfig.json_ 293 | 294 | > If you're feeling adventurous, you can use NodeJS **>=23.x** and omit the `--experimental-strip-types` flag. Keep an eye on this PR for when unflagged type-stripping support may come to Node LTS 22.x. 👀 295 | 296 | ## JSX 297 | 298 | > ⚠️ _Very Experimental!_ 299 | 300 | Even more experimental than WCC is the option to author a rendering function for native `HTMLElements`, that can compile down to a zero run time, web ready custom element! It handles resolving event handling and `this` references and can manage some basic re-rendering lifecycles. 301 | 302 | ### Example 303 | 304 | Below is an example of what is possible right now demonstrated through a [Counter component](https://github.com/thescientist13/greenwood-counter-jsx). 305 | 306 | ```jsx 307 | export default class Counter extends HTMLElement { 308 | constructor() { 309 | super(); 310 | this.count = 0; 311 | } 312 | 313 | connectedCallback() { 314 | this.render(); // this is required 315 | } 316 | 317 | increment() { 318 | this.count += 1; 319 | this.render(); 320 | } 321 | 322 | render() { 323 | const { count } = this; 324 | 325 | return ( 326 |
327 | 328 | You have clicked {count} times 329 | 330 |
331 | ); 332 | } 333 | } 334 | 335 | customElements.define('wcc-counter', Counter); 336 | ``` 337 | 338 | A couple things to observe in the above example: 339 | - The `this` reference is correctly bound to the `` element's state. This works for both `this.count` and the event handler, `this.increment`. 340 | - Event handlers need to manage their own render function updates. 341 | - `this.count` will know it is a member of the ``'s state, and so will re-run `this.render` automatically in the compiled output. 342 | 343 | > There is an [active discussion tracking features](https://github.com/ProjectEvergreen/wcc/discussions/84) and [issues in progress](https://github.com/ProjectEvergreen/wcc/issues?q=is%3Aopen+is%3Aissue+label%3AJSX) to continue iterating on this, so please feel free to try it out and give us your feedback! 344 | 345 | ### Prerequisites 346 | 347 | There are of couple things you will need to do to use WCC with JSX: 348 | 1. NodeJS version needs to be >= `20.10.0` 349 | 1. You will need to use the _.jsx_ extension 350 | 1. Requires the `--import` flag when invoking NodeJS 351 | ```shell 352 | $ NODE_OPTIONS="--import wc-compiler/register" node your-script.js 353 | ``` 354 | 355 | > _See our [example's page](/examples#jsx) for some usages of WCC + JSX._ 👀 356 | 357 | ### Declarative Shadow DOM 358 | 359 | To opt-in to Declarative Shadow DOM with JSX, you will need to signal to the WCC compiler your intentions so it can accurately mount from a `shadowRoot` on the client side. To opt-in, simply make a call to `attachShadow` in your `connectedCallback` method. 360 | 361 | Using, the Counter example from above, we would amend it like so: 362 | 363 | ```js 364 | export default class Counter extends HTMLElement { 365 | constructor() { 366 | super(); 367 | this.count = 0; 368 | } 369 | 370 | connectedCallback() { 371 | if (!this.shadowRoot) { 372 | this.attachShadow({ mode: 'open' }); // this is required for DSD support 373 | this.render(); 374 | } 375 | } 376 | 377 | // ... 378 | } 379 | 380 | customElements.define('wcc-counter', Counter); 381 | ``` 382 | 383 | ### (Inferred) Attribute Observability 384 | 385 | An optional feature supported by JSX based compilation is a feature called `inferredObservability`. With this enabled, WCC will read any `this` member references in your component's `render` function and map each member instance to 386 | - an entry in the `observedAttributes` array 387 | - automatically handle `attributeChangedCallback` update (by calling `this.render()`) 388 | 389 | So taking the above counter example, and opting in to this feature, we just need to enable the `inferredObservability` option in the component 390 | ```jsx 391 | export const inferredObservability = true; 392 | 393 | export default class Counter extends HTMLElement { 394 | ... 395 | 396 | render() { 397 | const { count } = this; 398 | 399 | return ( 400 |
401 | 402 | You have clicked {count} times 403 | 404 |
405 | ); 406 | } 407 | } 408 | ``` 409 | 410 | And so now when the attribute is set on this component, the component will re-render automatically, no need to write out `observedAttributes` or `attributeChangedCallback`! 411 | ```html 412 | 413 | ``` 414 | 415 | Some notes / limitations: 416 | - Please be aware of the above linked discussion which is tracking known bugs / feature requests / open items related to all things WCC + JSX. 417 | - We consider the capability of this observability to be "coarse grained" at this time since WCC just re-runs the entire `render` function, replacing of the `innerHTML` for the host component. Thought it is still WIP, we are exploring a more ["fine grained" approach](https://github.com/ProjectEvergreen/wcc/issues/108) that will more efficient than blowing away all the HTML, a la in the style of [**lit-html**](https://lit.dev/docs/templates/overview/) or [**Solid**'s Signals](https://www.solidjs.com/tutorial/introduction_signals). 418 | - This automatically _reflects properties used in the `render` function to attributes_, so YMMV. 419 | -------------------------------------------------------------------------------- /docs/pages/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Below are some example of how **WCC** is being used right now. 4 | 5 | ## Table of contents 6 | 7 | ## Server Rendering (SSR) 8 | 9 | For the project [**Greenwood**](https://www.greenwoodjs.dev/), **WCC** is used to provide a _Next.js_ like experience by allowing users to author [server-side routes using native custom elements](https://www.greenwoodjs.dev/docs/pages/server-rendering/#web-server-components)! ✨ 10 | 11 | ```js 12 | import '../components/card/card.js'; 13 | 14 | export default class ArtistsPage extends HTMLElement { 15 | async connectedCallback() { 16 | if (!this.shadowRoot) { 17 | const artists = await fetch('https://www.domain.com/api/artists') 18 | .then(resp => resp.json()); 19 | const html = artists.map(artist => { 20 | return ` 21 | 22 |

${artist.name}

23 | ${artist.name} 24 |
25 | `; 26 | }).join(''); 27 | 28 | this.attachShadow({ mode: 'open' }); 29 | this.shadowRoot.innerHTML = html; 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## Serverless and Edge Functions 36 | 37 | In the talk [_"Web Components at the Edge"_](https://sched.co/11loQ) for OpenJS World 2022, **WCC** was leveraged for all the AWS Lambda serverless function and Netlify Edge function demos. It also shows some clever ways to use **WCC** in more constrained runtime environments, like an edge runtime where something like `fs` might not be available. See all the [code, slides and demos in GitHub](https://github.com/thescientist13/web-components-at-the-edge). 🚀 38 | 39 | ```js 40 | import '../../node_modules/wc-compiler/src/dom-shim.js'; 41 | 42 | import Greeting from './components/greeting.js'; 43 | 44 | export default async function (request, context) { 45 | const countryCode = context.geo.country.code || 'UNKNOWN'; 46 | const countryName = context.geo.country.name || 'UNKNOWN'; 47 | const greeting = new Greeting(countryCode, countryName); 48 | 49 | greeting.connectedCallback(); 50 | 51 | const response = new Response(` 52 | 53 | 54 | 55 | 56 | ${greeting.getHTML({ serializableShadowRoots: true })} 57 |
58 |
 59 |               ${JSON.stringify(context.geo)}
 60 |             
61 |
62 |
63 | 64 | 65 | `); 66 | 67 | response.headers.set('content-type', 'text/html'); 68 | 69 | return response; 70 | } 71 | ``` 72 | 73 | ## Static Sites (SSG) 74 | 75 | Using `innerHTML`, custom elements can be authored to not use Shadow DOM, which can be useful for a `Layout` or `App` component where that top level content specifically should _not_ be rendered in a shadow root, e.g. `