Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@lit-labs/ssr

Package Overview
Dependencies
Maintainers
7
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lit-labs/ssr - npm Package Compare versions

Comparing version 1.0.0-pre.1 to 1.0.0-rc.1

CHANGELOG.md

3

index.d.ts

@@ -6,3 +6,4 @@ /**

*/
export {};
export * from './lib/render-lit-html.js';
export { ElementRenderer } from './lib/element-renderer.js';
//# sourceMappingURL=index.d.ts.map

@@ -6,3 +6,4 @@ /**

*/
export {};
export * from './lib/render-lit-html.js';
export { ElementRenderer } from './lib/element-renderer.js';
//# sourceMappingURL=index.js.map

@@ -7,12 +7,20 @@ /**

/**
* Constructs a fresh instance of the "window" vm context to use for
* evaluating user SSR code. Includes a minimal shim of DOM APIs.
* Constructs a fresh instance of the "window" vm context to use for evaluating
* user SSR code. Includes a minimal shim of DOM APIs.
*
* @param includeJSBuiltIns Whether certain standard JS context globals like
* `console` and `setTimeout` should also be added to the global. Should
* generally only be true when adding window to a fresh VM context that
* starts with nothing.
* @param props Additional properties to add to the window global
*/
export declare const getWindow: (props?: {
[key: string]: unknown;
export declare const getWindow: ({ includeJSBuiltIns, props, }: {
includeJSBuiltIns?: boolean | undefined;
props?: {} | undefined;
}) => {
[key: string]: unknown;
};
export declare const installWindowOnGlobal: (props?: {
[key: string]: unknown;
}) => void;
//# sourceMappingURL=dom-shim.d.ts.map

@@ -15,8 +15,12 @@ /**

/**
* Constructs a fresh instance of the "window" vm context to use for
* evaluating user SSR code. Includes a minimal shim of DOM APIs.
* Constructs a fresh instance of the "window" vm context to use for evaluating
* user SSR code. Includes a minimal shim of DOM APIs.
*
* @param includeJSBuiltIns Whether certain standard JS context globals like
* `console` and `setTimeout` should also be added to the global. Should
* generally only be true when adding window to a fresh VM context that
* starts with nothing.
* @param props Additional properties to add to the window global
*/
export const getWindow = (props = {}) => {
export const getWindow = ({ includeJSBuiltIns = false, props = {}, }) => {
const attributes = new WeakMap();

@@ -65,2 +69,8 @@ const attributesForElement = (element) => {

}
createTextNode() {
return {};
}
createElement() {
return {};
}
}

@@ -92,2 +102,3 @@ class CSSStyleSheet {

ShadowRoot,
CustomElementRegistry,
customElements: new CustomElementRegistry(),

@@ -97,31 +108,9 @@ btoa(s) {

},
console: {
log(...args) {
console.log(...args);
},
info(...args) {
console.info(...args);
},
warn(...args) {
console.warn(...args);
},
debug(...args) {
console.debug(...args);
},
error(...args) {
console.error(...args);
},
assert(bool, msg) {
if (!bool) {
throw new Error(msg);
}
},
fetch: (url, init) => fetch(url, init),
location: new URL('http://localhost'),
MutationObserver: class {
observe() { }
},
fetch: (url, init) => fetch(url, init),
// No-op any async tasks
requestAnimationFrame() { },
setTimeout() { },
clearTimeout() { },
// Required for node-fetch
Buffer,
// Set below

@@ -135,4 +124,51 @@ window: undefined,

window.global = window; // Required for node-fetch
if (includeJSBuiltIns) {
Object.assign(window, {
// No-op any async tasks
setTimeout() { },
clearTimeout() { },
// Required for node-fetch
Buffer,
console: {
log(...args) {
console.log(...args);
},
info(...args) {
console.info(...args);
},
warn(...args) {
console.warn(...args);
},
debug(...args) {
console.debug(...args);
},
error(...args) {
console.error(...args);
},
assert(bool, msg) {
if (!bool) {
throw new Error(msg);
}
},
},
});
}
return window;
};
export const installWindowOnGlobal = (props = {}) => {
// Avoid installing the DOM shim if one already exists
if (globalThis.window === undefined) {
const window = getWindow({ props });
// Setup window to proxy all globals added to window to the node global
window.window = new Proxy(window, {
set(_target, p, value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window[p] = globalThis[p] = value;
return true;
},
});
// Copy initial window globals to node global
Object.assign(globalThis, window);
}
};
//# sourceMappingURL=dom-shim.js.map

@@ -11,2 +11,5 @@ /// <reference lib="dom" />

import { RenderInfo } from './render-lit-html.js';
export declare type ElementRendererConstructor = (new (tagName: string) => ElementRenderer) & typeof ElementRenderer;
declare type AttributesMap = Map<string, string>;
export declare const getElementRenderer: ({ elementRenderers }: RenderInfo, tagName: string, ceClass?: typeof HTMLElement, attributes?: AttributesMap) => ElementRenderer | undefined;
/**

@@ -16,5 +19,16 @@ * An object that renders elements of a certain type.

export declare abstract class ElementRenderer {
element: HTMLElement;
constructor(element: HTMLElement);
element?: HTMLElement;
tagName: string;
/**
* Should be implemented to return true when the given custom element class
* and/or tagName should be handled by this renderer.
*
* @param ceClass - Custom Element class
* @param tagName - Tag name of custom element instance
* @param attributes - Map of attribute key/value pairs
* @returns
*/
static matchesClass(_ceClass: typeof HTMLElement, _tagName: string, _attributes: AttributesMap): boolean;
constructor(tagName: string);
/**
* Should implement server-appropriate implementation of connectedCallback

@@ -50,3 +64,3 @@ */

*/
abstract renderShadow(): IterableIterator<string>;
abstract renderShadow(_renderInfo: RenderInfo): IterableIterator<string> | undefined;
/**

@@ -63,2 +77,3 @@ * Render an element's light DOM children.

}
export {};
//# sourceMappingURL=element-renderer.d.ts.map
/// <reference lib="dom" />
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const escapeHtml = require('escape-html');
export const getElementRenderer = ({ elementRenderers }, tagName, ceClass = customElements.get(tagName), attributes = new Map()) => {
if (ceClass === undefined) {
console.warn(`Custom element ${tagName} was not registered.`);
return;
}
// TODO(kschaaf): Should we implement a caching scheme, e.g. keyed off of
// ceClass's base class to prevent O(n) lookups for every element (probably
// not a concern for the small number of element renderers we'd expect)? Doing
// so would preclude having cross-cutting renderers to e.g. no-op render all
// custom elements with a `client-only` attribute, so punting for now.
for (const renderer of elementRenderers) {
if (renderer.matchesClass(ceClass, tagName, attributes)) {
return new renderer(tagName);
}
}
return undefined;
};
/**

@@ -8,6 +27,18 @@ * An object that renders elements of a certain type.

export class ElementRenderer {
constructor(element) {
this.element = element;
constructor(tagName) {
this.tagName = tagName;
}
/**
* Should be implemented to return true when the given custom element class
* and/or tagName should be handled by this renderer.
*
* @param ceClass - Custom Element class
* @param tagName - Tag name of custom element instance
* @param attributes - Map of attribute key/value pairs
* @returns
*/
static matchesClass(_ceClass, _tagName, _attributes) {
return false;
}
/**
* Handles setting a property.

@@ -21,4 +52,6 @@ *

setProperty(name, value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.element[name] = value;
if (this.element !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.element[name] = value;
}
}

@@ -36,5 +69,7 @@ /**

setAttribute(name, value) {
const old = this.element.getAttribute(name);
this.element.setAttribute(name, value);
this.attributeChangedCallback(name, old, value);
if (this.element !== undefined) {
const old = this.element.getAttribute(name);
this.element.setAttribute(name, value);
this.attributeChangedCallback(name, old, value);
}
}

@@ -47,10 +82,12 @@ /**

*renderAttributes() {
const { attributes } = this.element;
for (let i = 0, name, value; i < attributes.length && ({ name, value } = attributes[i]); i++) {
if (value === '') {
yield ` ${name}`;
if (this.element !== undefined) {
const { attributes } = this.element;
for (let i = 0, name, value; i < attributes.length && ({ name, value } = attributes[i]); i++) {
if (value === '') {
yield ` ${name}`;
}
else {
yield ` ${name}="${escapeHtml(value)}"`;
}
}
else {
yield ` ${name}="${escapeHtml(value)}"`;
}
}

@@ -57,0 +94,0 @@ }

@@ -8,3 +8,3 @@ /**

/**
* Imports a module given by `path` into a new VM context with `sandbox` as the
* Imports a module given by `path` into a new VM context with `contextGlobal` as the
* global object.

@@ -14,5 +14,5 @@ *

* @param referrer
* @param sandbox The object that will become the global, via vm.createContext
* @param contextGlobal The object that will become the global, via vm.createContext
*/
export declare const importModule: (specifier: string, referrer: string, sandbox: vm.Context) => Promise<vm.Module>;
export declare const importModule: (specifier: string, referrer: string, contextGlobal: vm.Context) => Promise<vm.Module>;
//# sourceMappingURL=import-module.d.ts.map

@@ -80,3 +80,3 @@ /**

/**
* Imports a module given by `path` into a new VM context with `sandbox` as the
* Imports a module given by `path` into a new VM context with `contextGlobal` as the
* global object.

@@ -86,8 +86,8 @@ *

* @param referrer
* @param sandbox The object that will become the global, via vm.createContext
* @param contextGlobal The object that will become the global, via vm.createContext
*/
export const importModule = async (specifier, referrer, sandbox) => {
export const importModule = async (specifier, referrer, contextGlobal) => {
// TODO: maybe move this call outside this function and share across requests.
// Only if we can freeze all globals though.
const context = vm.createContext(sandbox);
const context = vm.createContext(contextGlobal);
vmContextId++;

@@ -94,0 +94,0 @@ // TODO: consider a multi-level cache with one cache shared across requests.

@@ -17,8 +17,9 @@ /**

element: LitElement;
constructor(element: LitElement);
static matchesClass(ctor: typeof HTMLElement): boolean;
constructor(tagName: string);
connectedCallback(): void;
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
renderShadow(): IterableIterator<string>;
renderShadow(renderInfo: RenderInfo): IterableIterator<string>;
renderLight(renderInfo: RenderInfo): IterableIterator<string>;
}
//# sourceMappingURL=lit-element-renderer.d.ts.map

@@ -9,3 +9,3 @@ /**

import { _Φ } from 'lit-element/private-ssr-support.js';
import { render, renderValue } from './render-lit-html.js';
import { render } from './render-lit-html.js';
const { attributeToProperty, changedProperties } = _Φ;

@@ -16,6 +16,9 @@ /**

export class LitElementRenderer extends ElementRenderer {
constructor(element) {
super(element);
this.element = element;
constructor(tagName) {
super(tagName);
this.element = new (customElements.get(this.tagName))();
}
static matchesClass(ctor) {
return ctor._$litElement$;
}
connectedCallback() {

@@ -34,3 +37,3 @@ // Call LitElement's `willUpdate` method.

}
*renderShadow() {
*renderShadow(renderInfo) {
// Render styles.

@@ -48,3 +51,3 @@ const styles = this.element.constructor

// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield* render(this.element.render());
yield* render(this.element.render(), renderInfo);
}

@@ -55,3 +58,3 @@ *renderLight(renderInfo) {

if (value) {
yield* renderValue(value, renderInfo);
yield* render(value, renderInfo);
}

@@ -58,0 +61,0 @@ else {

/// <reference lib="dom" />
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import type { TemplateResult } from 'lit';
import { ElementRenderer } from './element-renderer.js';
import { ElementRenderer, ElementRendererConstructor } from './element-renderer.js';
declare module 'parse5' {

@@ -15,3 +9,5 @@ interface DefaultTreeElement {

export declare type RenderInfo = {
elementRenderers: ElementRendererConstructor[];
customElementInstanceStack: Array<ElementRenderer | undefined>;
customElementHostStack: Array<ElementRenderer | undefined>;
};

@@ -23,5 +19,15 @@ declare global {

}
export declare function render(value: unknown): IterableIterator<string>;
export declare function renderValue(value: unknown, renderInfo: RenderInfo): IterableIterator<string>;
export declare function renderTemplateResult(result: TemplateResult, renderInfo: RenderInfo): IterableIterator<string>;
/**
* Renders a lit-html template (or any renderable lit-html value) to a string
* iterator. Any custom elements encountered will be rendered if a matching
* ElementRenderer is found.
*
* This method is suitable for streaming the contents of the element.
*
* @param value Value to render
* @param renderInfo Optional render context object that should be passed
* to any re-entrant calls to `render`, e.g. from a `renderShadow` callback
* on an ElementRenderer.
*/
export declare function render(value: unknown, renderInfo?: RenderInfo): IterableIterator<string>;
//# sourceMappingURL=render-lit-html.d.ts.map

@@ -7,3 +7,6 @@ /// <reference lib="dom" />

const { getTemplateHtml, marker, markerMatch, boundAttributeSuffix, overrideDirectiveResolve, getAttributePartCommittedValue, resolveDirective, AttributePart, PropertyPart, BooleanAttributePart, EventPart, } = _Σ;
import { digestForTemplateResult } from 'lit/hydrate.js';
import { digestForTemplateResult } from 'lit/experimental-hydrate.js';
import { getElementRenderer, } from './element-renderer.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-var-requires

@@ -13,5 +16,4 @@ const escapeHtml = require('escape-html');

import { isRenderLightDirective } from '@lit-labs/ssr-client/directives/render-light.js';
import { LitElement } from 'lit';
import { reflectedAttributeName } from './reflected-attributes.js';
import { LitElementRenderer } from './lit-element-renderer.js';
import { reflectedAttributeName } from './reflected-attributes.js';
const patchedDirectiveCache = new Map();

@@ -99,3 +101,5 @@ /**

* - Emit end of of open tag `>`
* - Emit `<!--lit-bindings n-->` marker if there were attribute parts
* - `possible-node-marker`
* - Emit `<!--lit-node n-->` marker if there were attribute parts or
* we needed to emit the `defer-hydration` attribute
* - `custom-element-shadow`

@@ -121,3 +125,3 @@ * - Emit `renderer.renderShadow()` (emits `<template shadowroot>` +

*/
const ast = parseFragment(html, {
const ast = parseFragment(String(html), {
sourceCodeLocationInfo: true,

@@ -174,9 +178,8 @@ });

lastOffset = offset;
const value = html.substring(previousLastOffset, offset);
const value = String(html).substring(previousLastOffset, offset);
flush(value);
};
// Depth-first node index. Initialized to -1 (corresponding to the fragment
// root node at the top of the ast) so that the first child node is
// index 0, to match client-side lit-html.
let nodeIndex = -1;
// Depth-first node index, counting only comment and element nodes, to match
// client-side lit-html.
let nodeIndex = 0;
traverse(ast, {

@@ -194,2 +197,3 @@ pre(node, parent) {

}
nodeIndex++;
}

@@ -201,3 +205,3 @@ else if (isElement(node)) {

let writeTag = false;
let boundAttrsCount = 0;
let boundAttributesCount = 0;
const tagName = node.tagName;

@@ -230,4 +234,4 @@ let ctor;

writeTag = true;
boundAttrsCount += 1;
// Note that although we emit a lit-bindings comment marker for any
boundAttributesCount += 1;
// Note that although we emit a lit-node comment marker for any
// nodes with bindings, we don't account for it in the nodeIndex because

@@ -294,6 +298,8 @@ // that will not be injected into the client template

}
ops.push({
type: 'possible-node-marker',
boundAttributesCount,
nodeIndex,
});
}
if (boundAttrsCount > 0) {
flush(`<!--lit-bindings ${nodeIndex}-->`);
}
if (ctor !== undefined) {

@@ -304,4 +310,4 @@ ops.push({

}
nodeIndex++;
}
nodeIndex++;
},

@@ -321,6 +327,24 @@ post(node) {

};
export function* render(value) {
yield* renderValue(value, { customElementInstanceStack: [] });
const defaultRenderInfo = {
elementRenderers: [LitElementRenderer],
customElementInstanceStack: [],
customElementHostStack: [],
};
/**
* Renders a lit-html template (or any renderable lit-html value) to a string
* iterator. Any custom elements encountered will be rendered if a matching
* ElementRenderer is found.
*
* This method is suitable for streaming the contents of the element.
*
* @param value Value to render
* @param renderInfo Optional render context object that should be passed
* to any re-entrant calls to `render`, e.g. from a `renderShadow` callback
* on an ElementRenderer.
*/
export function* render(value, renderInfo) {
renderInfo = { ...defaultRenderInfo, ...renderInfo };
yield* renderValue(value, renderInfo);
}
export function* renderValue(value, renderInfo) {
function* renderValue(value, renderInfo) {
patchIfDirective(value);

@@ -362,3 +386,3 @@ if (isRenderLightDirective(value)) {

}
export function* renderTemplateResult(result, renderInfo) {
function* renderTemplateResult(result, renderInfo) {
// In order to render a TemplateResult we have to handle and stream out

@@ -433,20 +457,4 @@ // different parts of the result separately:

case 'custom-element-open': {
const ctor = op.ctor;
// Instantiate the element and its renderer
let instance = undefined;
try {
const element = new ctor();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
element.tagName = op.tagName;
// TODO: Move renderer instantiation into a plugin system
if (element instanceof LitElement) {
instance = new LitElementRenderer(element);
}
else {
console.error(`No renderer for custom element: ${op.tagName}`);
}
}
catch (e) {
console.error('Exception in custom element constructor', e);
}
const instance = getElementRenderer(renderInfo, op.tagName, op.ctor, op.staticAttributes);
// Set static attributes to the element renderer

@@ -472,11 +480,35 @@ if (instance !== undefined) {

yield* instance.renderAttributes();
// If this element is nested in another, add the `defer-hydration`
// attribute, so that it does not enable before the host element
// hydrates
if (renderInfo.customElementHostStack.length > 0) {
yield ' defer-hydration';
}
}
break;
}
case 'possible-node-marker': {
// Add a node marker if this element had attribute bindings or if it
// was nested in another and we rendered the `defer-hydration` attribute
// since the hydration node walk will need to stop at this element
// to hydrate it
if (op.boundAttributesCount > 0 ||
renderInfo.customElementHostStack.length > 0) {
yield `<!--lit-node ${op.nodeIndex}-->`;
}
break;
}
case 'custom-element-shadow': {
const instance = getLast(renderInfo.customElementInstanceStack);
if (instance !== undefined && instance.renderShadow !== undefined) {
yield '<template shadowroot="open">';
yield* instance.renderShadow();
yield '</template>';
renderInfo.customElementHostStack.push(instance);
const shadowContents = instance.renderShadow(renderInfo);
// Only emit a DSR if renderShadow() emitted something (returning
// undefined allows effectively no-op rendering the element)
if (shadowContents !== undefined) {
yield '<template shadowroot="open">';
yield* shadowContents;
yield '</template>';
}
renderInfo.customElementHostStack.pop();
}

@@ -483,0 +515,0 @@ break;

@@ -20,5 +20,8 @@ /**

const window = getWindow({
// We need to give window a require to load CJS modules used by the SSR
// implementation. If we had only JS module dependencies, we wouldn't need this.
require: createRequire(import.meta.url),
includeJSBuiltIns: true,
props: {
// We need to give window a require to load CJS modules used by the SSR
// implementation. If we had only JS module dependencies, we wouldn't need this.
require: createRequire(import.meta.url),
},
});

@@ -25,0 +28,0 @@ const module = await importModule(specifier, referrer, window);

@@ -6,2 +6,4 @@ /**

*/
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-var-requires

@@ -8,0 +10,0 @@ const parse5 = require('parse5');

{
"name": "@lit-labs/ssr",
"type": "module",
"version": "1.0.0-pre.1",
"version": "1.0.0-rc.1",
"publishConfig": {

@@ -15,7 +15,6 @@ "access": "public"

"test": "npm run test:unit && npm run test:integration",
"test:integration": "node --experimental-vm-modules test/integration/test.js",
"test:unit": "node --experimental-vm-modules ./test/all-tests.js | tap-spec",
"link:dev": "rm -f node_modules/{lit-html,@lit/reactive-element,lit-element} && ln -s ../../lit-html/development node_modules/lit-html && ln -sf ../package.json ../lit-html/development/package.json && ln -s ../../reactive-element/development node_modules/@lit/reactive-element && ln -sf ../package.json ../reactive-element/development/package.json && ln -s ../../lit-element/development node_modules/lit-element && ln -sf ../package.json ../lit-element/development/package.json",
"link:prod": "rm -f node_modules/{lit-html,@lit/reactive-element,lit-element} && ln -s ../../lit-html node_modules/lit-html && ln -s ../../reactive-element node_modules/@lit/reactive-element && ln -s ../../lit-element node_modules/lit-element",
"link:check": "ls -l node_modules/{lit-html,@lit/reactive-element,lit-element}"
"test:integration": "npm run test:integration:dev && npm run test:integration:prod",
"test:integration:dev": "MODE=dev NODE_OPTIONS=--experimental-vm-modules wtr",
"test:integration:prod": "MODE=prod NODE_OPTIONS=--experimental-vm-modules wtr",
"test:unit": "node --experimental-vm-modules ./test/all-tests.js | tap-spec"
},

@@ -28,3 +27,7 @@ "files": [

"license": "BSD-3-Clause",
"repository": "PolymerLabs/lit-ssr",
"repository": {
"type": "git",
"url": "https://github.com/lit/lit.git",
"directory": "packages/labs/ssr"
},
"devDependencies": {

@@ -37,5 +40,5 @@ "@koa/router": "^8.0.8",

"@types/escape-html": "^1.0.0",
"@types/karma": "^4.4.1",
"@types/koa": "^2.0.49",
"@types/koa__router": "^8.0.2",
"@types/koa-cors": "*",
"@types/koa-static": "^4.0.1",

@@ -49,10 +52,8 @@ "@types/mocha": "^7.0.2",

"@types/tape-promise": "^4.0.0",
"@web/test-runner": "^0.12.19",
"chai": "^4.2.0",
"command-line-args": "^5.1.1",
"deepmerge": "^4.2.2",
"karma": "^4.4.1",
"karma-chai": "^0.1.0",
"karma-mocha": "^1.3.0",
"koa-cors": "^0.0.16",
"mocha": "^7.1.1",
"prettier": "^1.19.1",
"tap-spec": "^5.0.0",

@@ -64,4 +65,4 @@ "tape": "^4.11.0",

"dependencies": {
"@lit-labs/ssr-client": "^1.0.0-rc.1",
"@webcomponents/template-shadowroot": "^0.1.0",
"@lit-labs/ssr-client": "^1.0.0-pre.1",
"escape-html": "^1.0.3",

@@ -71,5 +72,5 @@ "koa": "^2.7.0",

"koa-static": "^5.0.0",
"lit": "^2.0.0-pre.2",
"lit-element": "^3.0.0-pre.4",
"lit-html": "^2.0.0-pre.7",
"lit": "^2.0.0-rc.1",
"lit-element": "^3.0.0-rc.1",
"lit-html": "^2.0.0-rc.1",
"node-fetch": "^2.6.0",

@@ -76,0 +77,0 @@ "parse5": "^5.1.0",

# @lit-labs/ssr
Lit SSR server
A package for server-side rendering Lit templates and components.
## Status
`@lit-labs/ssr` is pre-release software, not quite ready for public consumption. As we develop it we are using it as a test bed to ensure that the next versions of `lit-html` and `lit-element` are SSR-ready.
`@lit-labs/ssr` is pre-release software, not quite ready for public consumption. As we develop it we are using it as a test bed to ensure that new versions of `lit` (`lit-html` and `lit-element`) are SSR-ready. We expect that the foundational SSR support in this package will support a wide variety of use cases, from full-blown app rendering frameworks built on top of web components, to framework-specific plugins for rending custom elements in e.g. React or Angular, to pre-rendering plugins for static site generators like 11ty. Please stay tuned and file issues with use cases you'd like to see covered.
## Developing
## Server Usage
### Setup
### Rendering in the Node.js global scope
This package requires Node 14+ because of its use of experimental VM modules for creating VM sandbox for executing the client-side code in. It also requires
templates and elements be authored using pre-release versions of `lit-html@2.0` and `lit-element@3.0`.
The easiest way to get started is to import your Lit template modules (and any `LitElement` definitions they may use) into the node global scope and render them to a stream (or string) using the `render(value: unknown): Iterable<string>` function provided by the `render-with-global-dom-shim.js` module. Since Lit-authored code may rely on DOM globals, the `render-with-global-dom-shim.js` module will install a minimal DOM shim into the Node.js global scope, which should be sufficient for typical use cases. As such, `render-with-global-dom-shim.js` should be imported before any modules containing Lit code.
### Usage
```js
// Example: server.js:
#### Low-level API: render `lit-html` templates
import {render} from '@lit-labs/ssr/lib/render-with-global-dom-shim.js';
import {myTemplate} from './my-template.js';
As a low-level means of rendering `lit-html` templates (optionally containing
`LitElement` components), the `renderModule` function from the
`render-module` module will import a given module into a server-side VM
sandbox shimmed with the minimal DOM shim required for Lit server rendering,
//...
const ssrResult = render(myTemplate(data));
context.body = Readable.from(ssrResult);
```
### Rendering in a separate VM context
To avoid polluting the Node.js global object with the DOM shim and ensure each request receives a fresh global object, we also provide a way to load app code into, and render from, a separate VM context with its own global object. _Note that using this feature requires Node 14+ and passing the `--experimental-vm-modules` flag to node on because of its use of [experimental VM modules](https://nodejs.org/api/vm.html#vm_class_vm_sourcetextmodule) for creating a module-compatible VM context._
To render in a VM context, the `renderModule` function from the
`render-module.js` module will import a given module into a server-side VM
context shimmed with the minimal DOM shim required for Lit server rendering,
execute a given function exported from that module, and return its value.
Within that module, you an call the `render` method from the
`render-lit-html` module to render `lit-html` templates and return an async
iterable that emits the serialized strings of the given template.
Within that module, you can call the `render` method from the
`render-lit-html.js` module to render `lit-html` templates and return an
iterable that incrementally emits the serialized strings of the given template.
Example:
```js
// Example: render-template.js
import {render} from 'lit-ssr/lib/render-lit-html.js';
import {render} from '@lit-labs/ssr/lib/render-lit-html.js';
import {myTemplate} from './my-template.js';

@@ -46,5 +54,6 @@ export const renderTemplate = (someData) => {

import {renderModule} from 'lit-ssr/lib/render-module.js';
// Execute the above `renderTemplate` in a sandboxed VM with a minimal DOM shim
// Execute the above `renderTemplate` in a separate VM context with a minimal DOM shim
const ssrResult = await (renderModule(
'./render-template.js', // Module to load in SSR sandbox
'./render-template.js', // Module to load in VM context
import.meta.url, // Referrer URL for module

@@ -55,5 +64,89 @@ 'renderTemplate', // Function to call

for (let text of ssrResult) {
console.log(text);
}
// ...
context.body = Readable.from(ssrResult);
```
## Client usage
### Hydrating lit templates
"Hydration" is the process of having lit re-associate the expressions of a lit template with the nodes they should update in the DOM. In order to "hydrate" lit templates, the `hydrate` method from the `experimental-hydrate` module is provided in the `lit` package. Prior to updating a server-rendered container using `render`, you should first call `hydrate` on that container using the same template and data that was used to render on the server:
```js
import {myTemplate} from './my-template.js';
import {render, hydrate} from `lit/experimental-hydrate.js`;
// Initial hydration required before render:
// (must be same data used to render on the server)
const initialData = getInitialAppData();
hydrate(myTemplate(initialData), document.body);
// After hydration, render will efficiently update the server-rendered DOM:
const update = (data) => render(myTemplate(data), document.body);
```
### Hydrating LitElements
When `LitElement`s are server rendered, their shadow root contents are emitted inside a `<template shadowroot>`, also known as a [Declarative Shadow Root](https://web.dev/declarative-shadow-dom/), a new browesr feature that is shipping [Chrome](https://developer.chrome.com/blog/new-in-chrome-90/#declarative). Declarative shadow roots automatically attach their contents to a shadow root on the template's parent element when parsed. For browsers that do not yet implement declarative shadow root, there is a [`template-shadowroot`](https://github.com/webcomponents/template-shadowroot) ponyfill, described below.
Because the `hydrate` function above does not descend into shadow roots, it only works on one scope of the DOM at a time. To hydrate `LitElement` shadow roots, load the `lit/hydrate-support.js` module, which installs support for `LitElement` automatically hydrating itself when it detects it was server-rendered with declarative shadow DOM. This module should be loaded before the `lit` module is loaded, to ensure hydration support is properly installed.
Put together, an HTML page that was server rendered and containing `LitElement`s in the main document might look like this:
```js
import {render} from '@lit-labs/ssr/lib/render-with-global-dom-shim.js';
import './app-components.js';
const ssrResult = render(html`
<html>
<head>
</head>
<body>
<app-shell>
<app-page-one></app-page-one>
<app-page-two></app-page-two>
</app-component>
<script type="module">
// Hydrate template-shadowroots eagerly after rendering (for browsers without
// native declarative shadow roots)
import {
hasNativeDeclarativeShadowRoots,
hydrateShadowRoots
} from './node_modules/@webcomponents/template-shadowroot/template-shadowroot.js';
if (!hasNativeDeclarativeShadowRoots) {
hydrateShadowRoots(document.body);
}
// ...
// Load and hydrate components lazily
import('./app-components.js');
</script>
</body>
</html>
`);
// ...
context.body = Readable.from(ssrResult);
```
Note that as a simple example, the code above assumes a static top-level
template that does not need to be hydrated on the client, and that top-level
components individually hydrate themselves using data supplied either by
attributes or via a side-channel mechanism. This is in no way fundamental; the
top-level template can be used to pass data to the top-level components, and
that template can be loaded and hydrated on the client to apply the same data.
## Notes and limitations
Please note the following current limitations with the SSR package:
- **Browser support**: Support for hydrating elements on browsers that require the ShadyCSS polyfill (IE11) is not currently tested or supported.
- **DOM shim**: The DOM shim used by default is very minimal; because the server-rendering code for Lit relies largely on its declarative nature and string-based templates, we are able to incrementally stream the contents of a template and its sub-elements while avoiding the cost of a full DOM shim by not actually using the DOM for rendering. As such, the DOM shim used provides only enough to load and register element definitions, namely things like a minimal `HTMLElement` base class and `customElements` registry.
- **DOM access**: The above point means care should be taken to avoid interacting directly with the DOM in certain lifecycle callbacks. Concretely, you should generally only interact directly with the DOM (like accessing child/parent nodes, querying, imperatively adding event listeners, dispatching events, etc.) in the following lifecycle callbacks, which are not called on the server:
- `LitElement`'s `update`, `updated`, `firstUpdated`, or event handlers
- `Directive`'s `update`
- **Patterns for usage**: As mentioned above under "Status", we intend to flesh out a number of common patterns for using this package, and provide appropriate APIs and documentation for these as the package reaches maturity. Concerns like server/client data management, incremental loading and hydration, etc. are currently beyond the scope of what this package offers, but we believe it should support building these patterns on top of it going forwrad. Please [file issues](https://github.com/Polymer/lit-html/issues/new/choose) for ideas, suggestions, and use cases you may encounter.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc