diff options
Diffstat (limited to 'packages/renderers')
-rw-r--r-- | packages/renderers/renderer-lit/.gitignore | 1 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/client-shim.js | 9 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/index.js | 19 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/package.json | 23 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/readme.md | 78 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/server-shim.js | 2 | ||||
-rw-r--r-- | packages/renderers/renderer-lit/server.js | 64 |
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 +}; |