Skip to content

Commit

Permalink
update Lit renderer README and apply conditional Lit script hydration
Browse files Browse the repository at this point in the history
  • Loading branch information
thescientist13 committed Feb 27, 2024
1 parent 470b5c6 commit 200a40f
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 109 deletions.
6 changes: 4 additions & 2 deletions packages/cli/src/config/rollup.config.js
Expand Up @@ -358,7 +358,8 @@ const getRollupConfigForApis = async (compilation) => {
// support ESM favorable export conditions
// https://github.com/ProjectEvergreen/greenwood/issues/1118
nodeResolve({
exportConditions: ['default', 'module', 'import', 'node']
exportConditions: ['default', 'module', 'import', 'node'],
preferBuiltins: true
}),
commonjs(),
greenwoodImportMetaUrl(compilation)
Expand All @@ -383,7 +384,8 @@ const getRollupConfigForSsr = async (compilation, input) => {
// support ESM favorable export conditions
// https://github.com/ProjectEvergreen/greenwood/issues/1118
nodeResolve({
exportConditions: ['default', 'module', 'import', 'node']
exportConditions: ['default', 'module', 'import', 'node'],
preferBuiltins: true
}),
commonjs(),
greenwoodImportMetaUrl(compilation),
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/lifecycles/graph.js
Expand Up @@ -49,6 +49,7 @@ const generateGraph = async (compilation) => {
let customData = {};
let filePath;
let prerender = true;
let hydration = false;

/*
* check if additional nested directories exist to correctly determine route (minus filename)
Expand Down Expand Up @@ -130,7 +131,8 @@ const generateGraph = async (compilation) => {
const request = await requestAsObject(new Request(filenameUrl));

worker.on('message', async (result) => {
prerender = result.prerender;
prerender = result.prerender ?? false;
hydration = result.hydration ?? false;

if (result.frontmatter) {
result.frontmatter.imports = result.frontmatter.imports || [];
Expand Down Expand Up @@ -201,6 +203,7 @@ const generateGraph = async (compilation) => {
* title: a default value that can be used for <title></title>
* isSSR: if this is a server side route
* prerednder: if this should be statically exported
* prerednder: if this page needs hydration support
*/
pages.push({
data: customData || {},
Expand All @@ -220,7 +223,8 @@ const generateGraph = async (compilation) => {
template,
title,
isSSR: !isStatic,
prerender
prerender,
hydration
});
}
}
Expand Down
90 changes: 44 additions & 46 deletions packages/plugin-renderer-lit/README.md
Expand Up @@ -2,7 +2,7 @@

## Overview

A Greenwood plugin for using [**Lit**'s SSR capabilities](https://github.com/lit/lit/tree/main/packages/labs/ssr) as a custom server-side renderer. Although support is experimental at this time, this plugin also gives the ability to statically render entire pages and templates (instead of puppeteer) to output completely static sites.
A Greenwood plugin for using [**Lit**'s SSR capabilities](https://github.com/lit/lit/tree/main/packages/labs/ssr) as a custom server-side renderer. Although support is experimental at this time, this plugin also gives the ability to statically render entire pages and templates to output completely static sites.

_We are still actively working on SSR features and enhancements for Greenwood [as part of our 1.0 release](https://github.com/ProjectEvergreen/greenwood/issues?q=is%3Aissue+is%3Aopen+label%3Assr+milestone%3A1.0) so please feel free to test it out and report your feedback._ 🙏

Expand All @@ -11,7 +11,7 @@ _We are still actively working on SSR features and enhancements for Greenwood [a

## Prerequisite

This packages depends on the Lit package as a `peerDependency`. This means you must have Lit already installed in your project. You can install anything following the `2.x` release line.
This packages depends on the Lit package as a `peerDependency`. This means you must have Lit already installed in your project. You can install anything following the `3.x` release line.

```sh
# npm
Expand All @@ -33,7 +33,18 @@ npm install @greenwood/plugin-renderer-lit --save-dev
yarn add @greenwood/plugin-renderer-lit --dev
```

## Caveats

1. Please familiarize yourself with some of the [caveats](https://lit.dev/docs/ssr/overview/#library-status) called out in the Lit docs, like:
- Lit SSR [**only** renders into declarative shadow roots](https://github.com/lit/lit/issues/3080#issuecomment-1165158794), so you will have to keep browser support and polyfill usage in mind.
- At this time, `LitElement` does not support `async` work. You can follow along with this issue [in the Lit repo](https://github.com/lit/lit/issues/2469).
1. Lit only supports templates on the server side for HTML only generated content, thus Greenwood's `getBody` API must be used. We would love for [server only components](https://github.com/lit/lit/issues/2469#issuecomment-1759583861) to be a thing though!
1. Full hydration support is not available yet. See [this Greenwood issue](https://github.com/ProjectEvergreen/greenwood/issues/880) to follow along when it will land

> See [this repo](https://github.com/thescientist13/greenwood-lit-ssr) for a full demo of isomorphic Lit SSR with SSR pages and API routes deployed to Vercel.
## Usage

Add this plugin to your _greenwood.config.js_.

```javascript
Expand All @@ -48,58 +59,45 @@ export default {
}
```

Now, you can write some [SSR routes](/docs/server-rendering/) using Lit including all the [available APIs](docs/server-rendering/#api). The below example uses the standard [SimpleGreeting](https://lit.dev/playground/) component from the Lit docs by also using a LitElement as the `default export`!
```js
import { html, LitElement } from 'lit';
import './path/to/greeting.js';

export default class ArtistsPage extends LitElement {

constructor() {
super();
this.artists = [{ /* ... */ }];
}

render() {
const { artists } = this;

return html`
${
artists.map((artist) => {
const { id, name, imageUrl } = artist;
Now, you can author [SSR pages](/docs/server-rendering/) using Lit templates and components using Greenwood's [`getBody` API](https://www.greenwoodjs.io/docs/server-rendering/#usage). The below is an example of generating a template of LitElement based `<app-card>` web components.

return html`
<a href="/artists/${id}" target="_blank">
<simple-greeting .name="${name}"></simple-greeting>
</a>
<img src="${imageUrl}" loading="lazy"/>
<br/>
`;
})
}
`;
}
```js
// src/pages/products.js
import { html } from 'lit';
import '../components/card.js';

export async function getBody() {
const products = await getProducts();

return html`
${
products.map((product, idx) => {
const { title, thumbnail } = product;
return html`
<app-card
title="${idx + 1}) ${title}"
thumbnail="${thumbnail}"
></app-card>
`;
})
}
`;
}

// for now these are needed for the Lit specific implementations
customElements.define('artists-page', ArtistsPage);
export const tagName = 'artists-page';
```

## Caveats
## Options

There are a few considerations to take into account when using a `LitElement` as your page component:
- Lit SSR [**only** renders into declarative shadow roots](https://github.com/lit/lit/issues/3080#issuecomment-1165158794), so you will have to keep browser support and polyfill usage in mind.
- Depending on your use case, SSR bundling may break due to bundle chunking and code splitting by Rollup, which we are [hoping to correct ASAP](https://github.com/ProjectEvergreen/greenwood/issues/1118).
- At this time, `LitElement` does [not support `async` work](https://lit.dev/docs/ssr/overview/#library-status) which makes data fetching in pages a bit of challenge. You can follow along with this issue [in the Lit repo](https://github.com/lit/lit/issues/2469).
### Hydration

> _You can see a work (in progress) demo of using Lit SSR (with Serverless!) [here](https://github.com/thescientist13/greenwood-demo-adapter-vercel-lit/)._
In order for server-rendered components to become interactive on the client side, Lit's [client-side hydration script](https://lit.dev/docs/ssr/client-usage/#loading-@lit-labsssr-clientlit-element-hydrate-support.js) must be included on the page. For any page that would need this script added, you can simply `export` the **hydration** option from your page.

## Options
```js
// src/pages/products.js
export const hydration = true;
```

### Prerender (experimental)
### Prerender

The plugin provides a setting that can be used to override Greenwood's [default _prerender_](/docs/configuration/#prerender) implementation which uses [WCC](https://github.com/ProjectEvergreen/wcc), to use Lit instead.

Expand Down
19 changes: 6 additions & 13 deletions packages/plugin-renderer-lit/src/execute-route-module.js
Expand Up @@ -8,9 +8,8 @@ async function executeRouteModule({ moduleUrl, compilation, page, prerender, htm
template: null,
body: null,
frontmatter: null,
html: null
// hydrate: false,
// pageData: {}
html: null,
hydration: false
};

// prerender static content
Expand All @@ -24,17 +23,11 @@ async function executeRouteModule({ moduleUrl, compilation, page, prerender, htm
data.html = await collectResult(render(templateResult));
} else {
const module = await import(moduleUrl).then(module => module);
// const { getTemplate = null, getBody = null, getFrontmatter = null, hydration = false, loader } = module;
const { getTemplate = null, getBody = null, getFrontmatter = null } = module;
const { getTemplate = null, getBody = null, getFrontmatter = null, hydration = false } = module;

// if (hydration) {
// data.hydrate = true;
// }

// if (loader) {
// data.pageData = await loader(); // request, compilation, etc can go here
// console.log(data.pageData);
// }
if (hydration) {
data.hydration = true;
}

if (getBody) {
const templateResult = await getBody(compilation, page, data.pageData);
Expand Down
35 changes: 6 additions & 29 deletions packages/plugin-renderer-lit/src/index.js
@@ -1,58 +1,35 @@
// import { checkResourceExists } from '../../lib/resource-utils.js';
// import fs from 'fs/promises';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';

class LitHydrationResource extends ResourceInterface {
constructor(compilation, options) {
super(compilation, options);
// this.extensions = ['html'];
// this.contentType = 'text/html';
// this.libPath = '@greenwood/router/router.js';
}

// assumes Greenwood's standard-html plugin has tracked this metadata
// during resource serve lifecycle
async shouldIntercept(url) {
const { pathname } = url;
const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {};
// const { hydrate, pageData } = matchingRoute;

return matchingRoute.isSSR; // hydrate && pageData;
return matchingRoute.isSSR && matchingRoute.hydration;
}

async intercept(url, request, response) {
let body = await response.text();
const { pathname } = url;
const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {};
console.log('LIT intercept =>', { url, matchingRoute });

// TODO would be nice not to have to do this, but
// this hydrate lib is not showing up in greenwood build / serve
// TODO would be nice not have to manually set module-shim
// when we drop support for import-map shim - https://github.com/ProjectEvergreen/greenwood/pull/1115
const type = process.env.__GWD_COMMAND__ === 'develop' // eslint-disable-line no-underscore-dangle
? 'module-shim'
: 'module';

// TODO have to manually set module-shim?
body = body.replace('<head>', `
<head>
<!-- this needs to come first before any userland code -->
<script type="${type}" src="/node_modules/@lit-labs/ssr-client/lit-element-hydrate-support.js"></script>
`);

// TODO full hydration implementation?
// <script type="module" defer>
// // https://lit.dev/docs/ssr/client-usage/
// import { render } from 'lit';
// import { hydrate } from '@lit-labs/ssr-client'; // this will need to be in users package.json and / or import map
// import { getBody } from '../src/pages/products.js';

// globalThis.document.addEventListener('DOMContentLoaded', () => {
// const hydrationData = JSON.parse(document.getElementById('__GWD_HYDRATION_DATA__')?.textContent || '{"__noData__": true}')
// console.log('lets get hydrated!', { hydrationData });

// if(!hydrationData.__noData__) {
// hydrate(getBody({}, {}, hydrationData), window.document.body);
// }
// });
// </script>

return new Response(body);
}
}
Expand Down
Expand Up @@ -211,15 +211,6 @@ describe('Serve Greenwood With: ', function() {
expect(styles.length).to.equal(1);
});

// TODO this should be managed via a plugin, not in core
// https://github.com/ProjectEvergreen/greenwood/issues/728
// it('should have one <script> tag in the <head> for lit polyfills', function() {
// const scripts = Array.from(dom.window.document.querySelectorAll('head > script')).filter(tag => !tag.getAttribute('data-gwd'));

// expect(scripts.length).to.equal(1);
// expect(scripts[0].getAttribute('src').startsWith('/polyfill-support')).to.equal(true);
// });

it('should have the expected number of <tr> tags of content', function() {
const rows = dom.window.document.querySelectorAll('body > table tr');

Expand Down Expand Up @@ -266,6 +257,13 @@ describe('Serve Greenwood With: ', function() {
expect(aboutPageGraphData.data.author).to.equal('Project Evergreen');
expect(aboutPageGraphData.data.date).to.equal('01-01-2021');
});

it('should not have the expected lit hydration script in the <head>', function() {
const scripts = Array.from(dom.window.document.querySelectorAll('head script'))
.filter((script) => script.getAttribute('src')?.indexOf('lit-element-hydrate-support') >= 0);

expect(scripts.length).to.equal(0);
});
});

describe('Serve command with HTML route response using LitElement as a getPage export with an <app-footer> component', function() {
Expand Down
Expand Up @@ -2,17 +2,10 @@ import fs from 'fs';
import { html } from 'lit';
import '../components/footer.js';

// export const hydration = true;

// export async function loader(request = null) {
// const products = await getProducts();

// return { products };
// }
export const hydration = true;

export async function getBody() {
const users = JSON.parse(fs.readFileSync(new URL('../../artists.json', import.meta.url), 'utf-8'));
// const { products } = data;

return html`
<h1>Users Page</h1>
Expand Down

0 comments on commit 200a40f

Please sign in to comment.