Skip to content

Commit

Permalink
feat: make generic ssr controller
Browse files Browse the repository at this point in the history
  • Loading branch information
bennypowers committed Oct 9, 2024
1 parent e43e747 commit e524cbe
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 139 deletions.
185 changes: 86 additions & 99 deletions docs/_plugins/lit-ssr/lit.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @license
* @license based on code from eleventy-plugin-lit
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
Expand All @@ -9,115 +9,102 @@ const { pathToFileURL } = require('node:url');
// eslint-disable-next-line no-redeclare
const { Worker } = require('node:worker_threads');

module.exports = {
/**
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
* @param {{componentModules: string[]}} resolvedComponentModules
*/
configFunction(
eleventyConfig,
{ componentModules } = {}
) {
if (componentModules === undefined || componentModules.length === 0) {
// If there are no component modules, we could never have anything to
// render.
return;
}
// Lit SSR includes comment markers to track the outer template from
// the template we've generated here, but it's not possible for this
// outer template to be hydrated, so they serve no purpose.
function trimOuterMarkers(renderedContent) {
return renderedContent
.replace(/^((<!--[^<>]*-->)|(<\?>)|\s)+/, '')
.replace(/((<!--[^<>]*-->)|(<\?>)|\s)+$/, '');
}

const resolvedComponentModules = componentModules.map(
module => pathToFileURL(path.resolve(process.cwd(), module)).href
);

let worker;

const requestIdResolveMap = new Map();
let requestId = 0;

eleventyConfig.on('eleventy.before', async () => {
worker = new Worker(path.resolve(__dirname, './worker/worker.js'));

worker.on('error', err => {
// eslint-disable-next-line no-console
console.error(
'Unexpected error while rendering lit component in worker thread',
err
);
throw err;
});

let requestResolve;
const requestPromise = new Promise(resolve => {
requestResolve = resolve;
});

worker.on('message', message => {
switch (message.type) {
case 'initialize-response': {
requestResolve();
break;
}
/**
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
* @param {{componentModules: string[]}} resolvedComponentModules
*/
module.exports = function(eleventyConfig, { componentModules } = {}) {
if (componentModules === undefined || componentModules.length === 0) {
// If there are no component modules, we could never have anything to
// render.
return;
}

case 'render-response': {
const { id, rendered } = message;
const resolve = requestIdResolveMap.get(id);
if (resolve === undefined) {
throw new Error(
'@lit-labs/eleventy-plugin-lit received invalid render-response message'
);
}
resolve(rendered);
requestIdResolveMap.delete(id);
break;
}
}
});
const resolvedComponentModules = componentModules.map(module =>
pathToFileURL(path.resolve(process.cwd(), module)).href);

const message = {
type: 'initialize-request',
imports: resolvedComponentModules,
};
let worker;

worker.postMessage(message);
await requestPromise;
const requestIdResolveMap = new Map();
let requestId = 0;

eleventyConfig.on('eleventy.before', async function() {
worker = new Worker(path.resolve(__dirname, './worker/worker.js'));

worker.on('error', err => {
// eslint-disable-next-line no-console
console.error('Unexpected error while rendering lit component in worker thread', err);
throw err;
});

eleventyConfig.on('eleventy.after', async () => {
await worker.terminate();
let requestResolve;
const requestPromise = new Promise(resolve => {
requestResolve = resolve;
});

eleventyConfig.addTransform('render-lit', async function(content) {
if (!this.page.outputPath.endsWith('.html')) {
return content;
worker.on('message', message => {
switch (message.type) {
case 'initialize-response': {
requestResolve();
break;
}

case 'render-response': {
const { id, rendered } = message;
const resolve = requestIdResolveMap.get(id);
if (resolve === undefined) {
throw new Error(
'@lit-labs/eleventy-plugin-lit received invalid render-response message'
);
}
resolve(rendered);
requestIdResolveMap.delete(id);
break;
}
}
});

const message = {
type: 'initialize-request',
imports: resolvedComponentModules,
};

worker.postMessage(message);
await requestPromise;
});

const renderedContent = await new Promise(resolve => {
requestIdResolveMap.set(requestId, resolve);
const message = {
type: 'render-request',
id: requestId++,
content,
page: JSON.parse(JSON.stringify(this.page)),
};
worker.postMessage(message);
});

const outerMarkersTrimmed = trimOuterMarkers(renderedContent);
return outerMarkersTrimmed;
eleventyConfig.on('eleventy.after', async () => {
await worker.terminate();
});

eleventyConfig.addTransform('render-lit', async function(content) {
if (!this.page.outputPath.endsWith('.html')) {
return content;
}
);
},
};

// Lit SSR includes comment markers to track the outer template from
// the template we've generated here, but it's not possible for this
// outer template to be hydrated, so they serve no purpose.
const renderedContent = await new Promise(resolve => {
requestIdResolveMap.set(requestId, resolve);
const message = {
type: 'render-request',
id: requestId++,
content,
page: JSON.parse(JSON.stringify(this.page)),
};
worker.postMessage(message);
});

// TODO(aomarks) Maybe we should provide an option to SSR option to skip
// outer markers (though note there are 2 layers of markers due to the
// use of the unsafeHTML directive).
function trimOuterMarkers(renderedContent) {
return renderedContent
.replace(/^((<!--[^<>]*-->)|(<\?>)|\s)+/, '')
.replace(/((<!--[^<>]*-->)|(<\?>)|\s)+$/, '');
}
const outerMarkersTrimmed = trimOuterMarkers(renderedContent);
return outerMarkersTrimmed;
}
);
};

38 changes: 26 additions & 12 deletions docs/_plugins/lit-ssr/worker/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,38 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

/** @import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js' */
/** @import { ReactiveController } from 'lit' */

import { parentPort } from 'worker_threads';
import { render } from '@lit-labs/ssr';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { collectResult } from '@lit-labs/ssr/lib/render-result.js';

import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';

import { ssrControllerMap } from '@rhds/elements/lib/ssr-controller.js';

if (parentPort === null) {
throw new Error('worker.js must only be run in a worker thread');
}

let initialized = false;

/**
* @param {ReactiveController} controller
* @returns {controller is RHDSSSRController}
*/
function isRHDSSSRController(controller) {
return !!controller.isRHDSSSRController;
}

parentPort.on('message', async message => {
switch (message.type) {
case 'initialize-request': {
if (!initialized) {
const { imports } = message;
await Promise.all(imports.map(module => import(module)));
const response = { type: 'initialize-response' };
parentPort.postMessage(response);
await Promise.all(message.imports.map(module => import(module)));
parentPort.postMessage({ type: 'initialize-response' });
}
initialized = true;
break;
Expand All @@ -34,24 +45,27 @@ parentPort.on('message', async message => {
const { id, content, page } = message;
const result = render(unsafeHTML(content), {
elementRenderers: [
class UxdotPatternRenderer extends LitElementRenderer {
class RHDSSSRableRenderer extends LitElementRenderer {
* renderShadow(renderInfo) {
if (this.tagName === 'uxdot-pattern') {
this.element.ssrController.page = page;
yield [this.element.ssrController.hostUpdate()];
}
const controllers = ssrControllerMap.get(this.element);
yield controllers?.map(async x => {
if (isRHDSSSRController(x)) {
x.page = page;
await x.ssrSetup();
return [];
}
}) ?? [];
yield* super.renderShadow(renderInfo);
}
},
],
});
const rendered = await collectResult(result);
const response = {
parentPort.postMessage({
type: 'render-response',
id,
rendered,
};
parentPort.postMessage(response);
});
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { isServer } from 'lit';
import type { UxdotPattern } from './uxdot-pattern.js';
import { isServer } from 'lit';
import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js';

export class UxdotPatternSSRControllerClient {
/** Hydrate the results of SSR on the client */
export class UxdotPatternSSRControllerClient extends RHDSSSRController {
allContent?: Node;
htmlContent?: Node;
jsContent?: Node;
cssContent?: Node;
hasCss = false;
hasJs = false;
hostUpdate?(): void;
/**
* Hydrate the results of SSR on the client
* @param host no place like home
*/
constructor(public host: UxdotPattern) {
constructor(host: UxdotPattern) {
super(host);
const { shadowRoot, hasUpdated } = this.host;
if (!isServer && shadowRoot && !hasUpdated) {
this.allContent ||= shadowRoot.getElementById('content')!;
Expand Down
Loading

0 comments on commit e524cbe

Please sign in to comment.