summaryrefslogtreecommitdiff
path: root/packages/renderers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/renderers')
-rw-r--r--packages/renderers/renderer-lit/.gitignore1
-rw-r--r--packages/renderers/renderer-lit/client-shim.js9
-rw-r--r--packages/renderers/renderer-lit/index.js19
-rw-r--r--packages/renderers/renderer-lit/package.json23
-rw-r--r--packages/renderers/renderer-lit/readme.md78
-rw-r--r--packages/renderers/renderer-lit/server-shim.js2
-rw-r--r--packages/renderers/renderer-lit/server.js64
7 files changed, 196 insertions, 0 deletions
diff --git a/packages/renderers/renderer-lit/.gitignore b/packages/renderers/renderer-lit/.gitignore
new file mode 100644
index 000000000..40b878db5
--- /dev/null
+++ b/packages/renderers/renderer-lit/.gitignore
@@ -0,0 +1 @@
+node_modules/ \ No newline at end of file
diff --git a/packages/renderers/renderer-lit/client-shim.js b/packages/renderers/renderer-lit/client-shim.js
new file mode 100644
index 000000000..8dd75826f
--- /dev/null
+++ b/packages/renderers/renderer-lit/client-shim.js
@@ -0,0 +1,9 @@
+async function polyfill() {
+ const { hydrateShadowRoots } = await import('@webcomponents/template-shadowroot/template-shadowroot.js');
+ hydrateShadowRoots(document.body);
+}
+
+if(!(new DOMParser().parseFromString(`<p><template shadowroot="open"></template></p>`, 'text/html', {
+ includeShadowRoots: true
+}).querySelector('p')?.shadowRoot))
+ polyfill(); \ No newline at end of file
diff --git a/packages/renderers/renderer-lit/index.js b/packages/renderers/renderer-lit/index.js
new file mode 100644
index 000000000..38b59a6f7
--- /dev/null
+++ b/packages/renderers/renderer-lit/index.js
@@ -0,0 +1,19 @@
+export default {
+ name: '@astrojs/renderer-lit',
+ server: './server.js',
+ external: [
+ '@lit-labs/ssr/lib/install-global-dom-shim.js',
+ '@lit-labs/ssr/lib/render-lit-html.js',
+ '@lit-labs/ssr/lib/lit-element-renderer.js'
+ ],
+ polyfills: [
+ './client-shim.js'
+ ],
+ hydrationPolyfills: [
+ 'lit/experimental-hydrate-support.js'
+ ],
+ knownEntrypoints: [
+ '@astrojs/renderer-lit/client-shim.js',
+ '@webcomponents/template-shadowroot/template-shadowroot.js'
+ ]
+};
diff --git a/packages/renderers/renderer-lit/package.json b/packages/renderers/renderer-lit/package.json
new file mode 100644
index 000000000..7c01c9891
--- /dev/null
+++ b/packages/renderers/renderer-lit/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@astrojs/renderer-lit",
+ "version": "0.1.0",
+ "description": "A Lit renderer for Astro",
+ "type": "module",
+ "exports": {
+ ".": "./index.js",
+ "./server.js": "./server.js",
+ "./client-shim.js": "./client-shim.js",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "index.js",
+ "client-shim.js",
+ "server.js",
+ "server-shim.js"
+ ],
+ "dependencies": {
+ "@lit-labs/ssr": "^1.0.0-rc.1",
+ "@webcomponents/template-shadowroot": "^0.1.0",
+ "lit": "^2.0.0-rc.2"
+ }
+}
diff --git a/packages/renderers/renderer-lit/readme.md b/packages/renderers/renderer-lit/readme.md
new file mode 100644
index 000000000..91ae2f390
--- /dev/null
+++ b/packages/renderers/renderer-lit/readme.md
@@ -0,0 +1,78 @@
+# Astro Lit Renderer
+
+This is a plugin for [Astro](https://astro.build/) apps that enables server-side rendering of custom elements build with [Lit](https://lit.dev/).
+
+Server-side rendering uses [Declarative Shadow DOM](https://web.dev/declarative-shadow-dom/), a new web technology that allows custom elements to be rendered to HTML with __0 JavaScript__.
+
+## Installation
+
+Install `@astrojs/renderer-lit` and then add it to your `astro.config.mjs` in the `renderers` property:
+
+```
+npm install @astrojs/renderer-lit
+```
+
+__astro.config.mjs__
+
+```js
+export default {
+ // ...
+
+ renderers: [
+ // ...
+ '@astrojs/renderer-lit'
+ ]
+}
+```
+
+## Usage
+
+If you're familiar with Astro then you already know that you can import components into your templates and use them. What's different about custom elements is you can use the *tag name* directly.
+
+Astro needs to know which tag is associated with which component script. We expose this through exporting a `tagName` variable from the component script. It looks like this:
+
+__src/components/my-element.js__
+
+```js
+import { LitElement, html } from 'lit';
+
+export const tagName = 'my-counter';
+
+class Counter extends LitElement {
+
+}
+
+customElements.define(tagName, Counter);
+```
+
+> Note that exporting the `tagName` is __required__ if you want to use the tag name in your templates. Otherwise you can export and use the constructor, like with non custom element frameworks.
+
+In your Astro template import this component as a side-effect and use the element.
+
+__src/pages/index.astro__
+
+```jsx
+---
+import '../components/my-element.js';
+---
+
+<my-element></my-element>
+```
+
+> Note that Lit requires browser globals such as `HTMLElement` and `customElements` to be present. For this reason the Lit renderer shims the server with these globals so Lit can run. You *might* run into libraries that work incorrectly because of this.
+
+### Polyfills & Hydration
+
+The renderer automatically handles adding appropriate polyfills for support in browsers that don't have Declarative Shadow DOM. The polyfill is about *1.5kB*. If the browser does support Declarative Shadow DOM then less than 250 bytes are loaded (to feature detect support).
+
+Hydration is also handled automatically. You can use the same hydration directives such as `client:load`, `client:idle` and `client:visible` as you can with other libraries that Astro supports.
+
+```jsx
+---
+import '../components/my-element.js';
+---
+
+<my-element client:visible />
+```
+
+The above will only load the element's JavaScript when the user has scrolled it into view. Since it is server rendered they will not see any jank; it will load and hydrate transparently. \ No newline at end of file
diff --git a/packages/renderers/renderer-lit/server-shim.js b/packages/renderers/renderer-lit/server-shim.js
new file mode 100644
index 000000000..89de78117
--- /dev/null
+++ b/packages/renderers/renderer-lit/server-shim.js
@@ -0,0 +1,2 @@
+import '@lit-labs/ssr/lib/install-global-dom-shim.js';
+document.getElementsByTagName = () => []; \ No newline at end of file
diff --git a/packages/renderers/renderer-lit/server.js b/packages/renderers/renderer-lit/server.js
new file mode 100644
index 000000000..83e1d51b4
--- /dev/null
+++ b/packages/renderers/renderer-lit/server.js
@@ -0,0 +1,64 @@
+import './server-shim.js';
+import '@lit-labs/ssr/lib/render-lit-html.js';
+import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
+
+function isCustomElementTag(name) {
+ return typeof name === 'string' && /-/.test(name);
+}
+
+function getCustomElementConstructor(name) {
+ if(typeof customElements !== 'undefined' && isCustomElementTag(name)) {
+ return customElements.get(name) || null;
+ }
+ return null;
+}
+
+async function isLitElement(Component) {
+ const Ctr = getCustomElementConstructor(Component);
+ return !!(Ctr && Ctr._$litElement$);
+}
+
+async function check(Component, _props, _children) {
+ // Lit doesn't support getting a tagName from a Constructor at this time.
+ // So this must be a string at the moment.
+ return !!(await isLitElement(Component));
+}
+
+function * render(tagName, attrs, children) {
+ const instance = new LitElementRenderer(tagName);
+
+ // LitElementRenderer creates a new element instance, so copy over.
+ for(let [name, value] of Object.entries(attrs)) {
+ instance.setAttribute(name, value);
+ }
+
+ yield `<${tagName}`;
+ yield* instance.renderAttributes();
+ yield `>`;
+ const shadowContents = instance.renderShadow({});
+ if (shadowContents !== undefined) {
+ yield '<template shadowroot="open">';
+ yield* shadowContents;
+ yield '</template>';
+ }
+ yield children;
+ yield `</${tagName}>`;
+}
+
+async function renderToStaticMarkup(Component, props, children) {
+ let tagName = Component;
+
+ let out = '';
+ for(let chunk of render(tagName, props, children)) {
+ out += chunk;
+ }
+
+ return {
+ html: out
+ };
+}
+
+export default {
+ check,
+ renderToStaticMarkup
+};