Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/issue 901 lit renderer DSD polyfill #922

Closed
Closed
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
27 changes: 24 additions & 3 deletions packages/plugin-renderer-lit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default {
...

plugins: [
greenwoodPluginRendererLit()
...greenwoodPluginRendererLit() // mind the spread!
]
}
```
Expand Down Expand Up @@ -106,11 +106,32 @@ export default {
...

plugins: [
greenwoodPluginRendererLit({
...greenwoodPluginRendererLit({
prerender: true
})
]
}
```

> _Keep in mind you will need to make sure your Lit Web Components are isomorphic and [properly leveraging `LitElement`'s lifecycles](https://github.com/lit/lit/tree/main/packages/labs/ssr#notes-and-limitations) and browser / Node APIs accordingly for maximum compatibility and portability._
> _Keep in mind you will need to make sure your Lit Web Components are isomorphic and [properly leveraging `LitElement`'s lifecycles](https://github.com/lit/lit/tree/main/packages/labs/ssr#notes-and-limitations) and browser / Node APIs accordingly for maximum compatibility and portability._

### Polyfill

If using the `prerender` feature, you'll need a polyfill to support browsers that don't understand Declarative Shadow DOM, which is the technique Lit uses when server rendering Web Components.

This option will append [this polyfill script](https://web.dev/declarative-shadow-dom/#polyfill) before the closing `</body>` tag which will make the inert `<template>` tag generated by Lit SSR "active" in the browser. Otherwise, any of the SSR content will not be visible by the user in the browser.

```javascript
import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit';

export default {
...

plugins: [
...greenwoodPluginRendererLit({
prerender: true,
polyfill: true
})
]
}
```
12 changes: 9 additions & 3 deletions packages/plugin-renderer-lit/src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { PolyfillsResource } from './polyfill-resource.js';

const greenwoodPluginRendererLit = (options = {}) => {
return {
return [{
type: 'renderer',
name: 'plugin-renderer-lit',
name: 'plugin-renderer-lit:renderer',
provider: () => {
return {
workerUrl: new URL('./ssr-route-worker-lit.js', import.meta.url),
prerender: options.prerender
};
}
};
}, {
type: 'resource',
name: 'plugin-renderer-lit:resource',
provider: (compilation) => new PolyfillsResource(compilation, options)
}];
};

export {
Expand Down
41 changes: 41 additions & 0 deletions packages/plugin-renderer-lit/src/polyfill-resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import path from 'path';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';

class PolyfillsResource extends ResourceInterface {
constructor(compilation, options = {}) {
super(compilation, options);
}

async shouldOptimize(url = '', body, headers = {}) {
return Promise.resolve(this.options.polyfill && path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0));
}

async optimize(url, body) {
return new Promise(async (resolve, reject) => {
try {
const newHtml = body.replace('</body>', `
<script>
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
(function attachShadowRoots(root) {
root.querySelectorAll("template[shadowroot]").forEach(template => {
const mode = template.getAttribute("shadowroot");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);
}
</script>
</body>
`);

resolve(newHtml);
} catch (e) {
reject(e);
}
});
}
}

export { PolyfillsResource };
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@
* greenwood build
*
* User Config
* import { greenwoodPluginIncludeHTML } from '@greenwod/plugin-include-html';
*
* {
* plugins: [{
* export default {
* plugins: [
* greenwoodPluginRendererLit({
* prerender: true
* })
* }]
* }
* ]
* };
*
* User Workspace
* src/
Expand Down Expand Up @@ -48,7 +46,7 @@ import { fileURLToPath, URL } from 'url';

const expect = chai.expect;

describe('Build Greenwood With Custom Lit Renderer for SSG prerendering: ', function() {
describe('Build Greenwood With Custom Lit Renderer: ', function() {
const LABEL = 'For SSG prerendering of Getting Started example';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { greenwoodPluginRendererLit } from '../../../src/index.js';

export default {
plugins: [
greenwoodPluginRendererLit({
...greenwoodPluginRendererLit({
prerender: true
})
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "plugin-prerender-lit-build-config-prerender-getting-started",
"type": "module",
"dependencies": {
"lit": "^2.1.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { greenwoodPluginRendererLit } from '../../../src/index.js';

export default {
plugins: [
...greenwoodPluginRendererLit({
polyfill: true
})
]
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "plugin-prerender-lit-build-prerender-getting-started",
"name": "plugin-prerender-lit-build-config-polyfill",
"type": "module",
"dependencies": {
"lit": "^2.1.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Use Case
* Run Greenwood with an SSR route.
*
* User Result
* Should generate a Greenwood build for hosting a server rendered application with polyfills.
*
* User Command
* greenwood build
*
* User Config
* export default {
* plugins: [
* greenwoodPluginRendererLit({
* polyfill: true
* })
* ]
* };
*
* User Workspace
* src/
* components/
* footer.js
* pages/
* artists.js
* templates/
* app.html
*/
import chai from 'chai';
import { JSDOM } from 'jsdom';
import path from 'path';
import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import request from 'request';
import { Runner } from 'gallinago';
import { fileURLToPath, URL } from 'url';

const expect = chai.expect;

describe('Serve Greenwood With Custom Lit Rendering for SSR: ', function() {
const LABEL = 'With Polyfills for Declarative Shadow DOM';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
const hostname = 'http://127.0.0.1:8080';
let runner;

before(async function() {
this.context = {
publicDir: path.join(outputPath, 'public')
};
runner = new Runner();
});

describe(LABEL, function() {

before(async function() {
const lit = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/*.js`,
`${outputPath}/node_modules/lit/`
);
const litDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/decorators/*.js`,
`${outputPath}/node_modules/lit/decorators/`
);
const litDirectives = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/directives/*.js`,
`${outputPath}/node_modules/lit/directives/`
);
const litPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/package.json`,
`${outputPath}/node_modules/lit/`
);
const litElement = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/*.js`,
`${outputPath}/node_modules/lit-element/`
);
const litElementPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/package.json`,
`${outputPath}/node_modules/lit-element/`
);
const litElementDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/decorators/*.js`,
`${outputPath}/node_modules/lit-element/decorators/`
);
const litHtml = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/*.js`,
`${outputPath}/node_modules/lit-html/`
);
const litHtmlPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/package.json`,
`${outputPath}/node_modules/lit-html/`
);
const litHtmlDirectives = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/directives/*.js`,
`${outputPath}/node_modules/lit-html/directives/`
);
// lit-html has a dependency on this
// https://github.com/lit/lit/blob/main/packages/lit-html/package.json#L82
const trustedTypes = await getDependencyFiles(
`${process.cwd()}/node_modules/@types/trusted-types/package.json`,
`${outputPath}/node_modules/@types/trusted-types/`
);
const litReactiveElement = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/*.js`,
`${outputPath}/node_modules/@lit/reactive-element/`
);
const litReactiveElementDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`,
`${outputPath}/node_modules/@lit/reactive-element/decorators/`
);
const litReactiveElementPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/package.json`,
`${outputPath}/node_modules/@lit/reactive-element/`
);

await runner.setup(outputPath, [
...getSetupFiles(outputPath),
...lit,
...litPackageJson,
...litDirectives,
...litDecorators,
...litElementPackageJson,
...litElement,
...litElementDecorators,
...litHtmlPackageJson,
...litHtml,
...litHtmlDirectives,
...trustedTypes,
...litReactiveElement,
...litReactiveElementDecorators,
...litReactiveElementPackageJson
]);

return new Promise(async (resolve) => {
setTimeout(() => {
resolve();
}, 10000);

await runner.runCommand(cliPath, 'serve');
});
});

let response = {};
let dom;

before(async function() {
return new Promise((resolve, reject) => {
request.get(`${hostname}/artists/`, (err, res, body) => {
if (err) {
reject();
}

response = res;
response.body = body;
dom = new JSDOM(body);

resolve();
});
});
});

describe('Serve command with Polyfill in the HTML route response', function() {

it('should return a 200 status', function(done) {
expect(response.statusCode).to.equal(200);
done();
});

it('should return the correct content type', function(done) {
expect(response.headers['content-type']).to.contain('text/html');
done();
});

it('should return a response body', function(done) {
expect(response.body).to.not.be.undefined;
done();
});

it('the response body should be valid HTML from JSDOM', function(done) {
expect(dom).to.not.be.undefined;
done();
});

it('should have one <style> tag in the <head>', function() {
const scripts = dom.window.document.querySelectorAll('body > script');

expect(scripts.length).to.equal(1);
expect(scripts[0].textContent).to.contain('if (!HTMLTemplateElement.prototype.hasOwnProperty(\'shadowRoot\')) {');
expect(scripts[0].textContent).to.contain('const shadowRoot = template.parentNode.attachShadow({ mode });');
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
runner.stopCommand();
});

});
Loading