Détail du package

@lit-labs/ssr

lit211.4kBSD-3-Clause3.3.1

SSR for Lit

readme

@lit-labs/ssr

A package for server-side rendering Lit templates and components.

[!WARNING]

This package is part of Lit Labs. It is published in order to get feedback on the design and may receive breaking changes or stop being supported.

Please read our Lit Labs documentation before using this library in production.

Documentation: https://lit.dev/docs/ssr/overview/

Give feedback: https://github.com/lit/lit/discussions/3353

Status

@lit-labs/ssr is pre-release software, not quite ready for public consumption. If you try Lit SSR, please give feedback and file issues with bugs and use cases you'd like to see covered.

Server Usage

Rendering in the Node.js global scope

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, renderInfo?: Partial<RenderInfo>): RenderResult function provided by @lit-labs/ssr. When running in Node, Lit automatically depends on Node-compatible implementations of a minimal set of DOM APIs provided by the @lit-labs/ssr-dom-shim package, including defining customElements on the global object.

Rendering to a stream

Web servers should prefer rendering to a stream, as they have a lower memory footprint and allow sending data in chunks as they are being processed. For this case use RenderResultReadable, which is a Node Readable stream implementation that provides values from RenderResult. This can be piped into a Writable stream, or passed to web server frameworks like Koa.

// Example: server.js:

import {render} from '@lit-labs/ssr';
import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js';
import {myTemplate} from './my-template.js';

//...

const ssrResult = render(myTemplate(data));
// Assume `context` is a Koa.Context.
context.body = new RenderResultReadable(ssrResult);

Rendering to a string

To render to a string, you can use the collectResult or collectResultSync helper functions.

import {render} from '@lit-labs/ssr';
import {
  collectResult,
  collectResultSync,
} from '@lit-labs/ssr/lib/render-result.js';
import {html} from 'lit';

const myServerTemplate = (name) => html`<p>Hello ${name}</p>`;
const ssrResult = render(myServerTemplate('SSR with Lit!'));

// Will throw if a Promise is encountered
console.log(collectResultSync(ssrResult));
// Awaits promises
console.log(await collectResult(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 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 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: render-template.js

import {render} from '@lit-labs/ssr';
import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js';
import {myTemplate} from './my-template.js';
export const renderTemplate = (someData) => {
  return render(myTemplate(someData));
};
// Example: server.js:

import {renderModule} from '@lit-labs/ssr/lib/render-module.js';

// 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 VM context
  import.meta.url,         // Referrer URL for module
  'renderTemplate',        // Function to call
  [{some: "data"}]         // Arguments to function
) as Promise<Iterable<unknown>>);

// ...

// Assume `context` is a Koa.Context, or other API that accepts a Readable.
context.body = new RenderResultReadable(ssrResult);

Client usage

Hydrating Lit templates

"Hydration" is the process of re-associating expressions in a template with the nodes they should update in the DOM. Hydration is performed by the hydrate() function from the @lit-labs/ssr-client module.

Prior to updating a server-rendered container using render(), you must first call hydrate() on that container using the same template and data that was used to render on the server:

import {myTemplate} from './my-template.js';
import {render} from 'lit';
import {hydrate} from '@lit-labs/ssr-client';
// 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 LitElements are server rendered, their shadow root contents are emitted inside a <template shadowroot>, also known as a Declarative Shadow Root, a new browser feature that is shipping in most modern browsers. 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 polyfill, described below.

hydrate() 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-labs/ssr-client/lit-element-hydrate-support.js module, which installs support for LitElement to automatically hydrate itself when it detects it was server-rendered with declarative shadow DOM. This module must 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 LitElements in the main document might look like this:

import {html} from 'lit';
import {render} from '@lit-labs/ssr';
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-shell>

      <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.

Server-only templates

@lit-labs/ssr also exports an html template function, similar to the normal Lit html function, only it's used for server-only templates. These templates can be used for rendering full documents, including the <!DOCTYPE html>, and rendering into elements that Lit normally cannot, like <title>, <textarea>, <template>, and safe <script> tags like <script type="text/json">. They are also slightly more efficient than normal Lit templates, because the generated HTML doesn't need to include markers for updating.

Server-only templates can be composed, and combined, and they support almost all features that normal Lit templates do, with the exception of features that don't have a pure HTML representation, like event handlers or property bindings.

Server-only templates can only be rendered on the server, they can't be rendered on the client. However if you render a normal Lit template inside a server-only template, then it can be hydrated and updated. Likewise, if you place a custom element inside a server-only template, it can be hydrated and update like normal.

Here's an example that shows how to use a server-only template to render a full document, and then lazily hydrate both a custom element and a template:

import {render, html} from '@lit-labs/ssr';
import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js';
import './app-shell.js';
import {getContent} from './content-template.js';

const pageInfo = {
  /* ... */
};

const ssrResult = render(html`
  <!DOCTYPE html>
  <html>
    <head><title>MyApp ${pageInfo.title}</head>
    <body>
      <app-shell>
        <!-- getContent is hydratable, as it returns a normal Lit template -->
        <div id="content">${getContent(pageInfo.description)}</div>
      </app-shell>

      <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';
        import {hydrate} from '@lit-labs/ssr-client';
        import {getContent} from './content-template.js';
        if (!hasNativeDeclarativeShadowRoots()) {
          hydrateShadowRoots(document.body);
        }

        // Load and hydrate app-shell lazily
        import('./app-shell.js');

        // Hydrate content template. This <script type=module> will run after
        // the page has loaded, so we can count on page-id being present.
        const pageInfo = JSON.parse(document.getElementById('page-info').textContent);
        hydrate(getContent(pageInfo.description), document.querySelector('#content'));
        // #content element can now be efficiently updated
      </script>
      <!-- Pass data to client. -->
      <script type="text/json" id="page-info">
        ${JSON.stringify(pageInfo)}
      </script>
    </body>
  </html>
`);

// ...

context.body = new RenderResultReadable(ssrResult);

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, etc.) in the following lifecycle callbacks, which are not called on the server:
    • LitElement's update, updated, firstUpdated
    • Directive's update
  • Events: The DOM shim implements basic event handling.
    • It is possible to register event handlers and to dispatch events, but only on custom elements.
    • Be aware that you cannot mutate a parent via events, due to the way we stream data via SSR. This means only a use case like @lit/context is supported, where events are used to pass data from a parent back to a child to use in its rendering.
    • The DOM tree is not fully formed and the event.composedPath() returns a simplified tree, only containing the custom elements.
    • As an alternative to document.documentElement or document.body (which are expected to be undefined in the server environment) for global event listeners (e.g. for @lit/context ContextProvider), you can use the global variable globalThis.litServerRoot which is (only) available during SSR (e.g. new ContextProvider(isServer ? globalThis.litServerRoot : document.body, {...})).
  • connectedCallback Opt-In: We provide an opt-in for calling connectedCallback, which can be enabled via globalThis.litSsrCallConnectedCallback = true;. This will enable calling connectedCallback (and the hostConnected hook for controllers, but not the hostUpdate hook) on the server. This e.g. enables using @lit/context on the server.
  • 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 forward. Please file issues for ideas, suggestions, and use cases you may encounter.

changelog

2020-09-20

lit - 2.0.0

Major Changes

  • New package serving as the main entry point for all users of Lit (including LitElement, ReactiveElement, and lit-html). See the Migration Guide for more details.

lit-element - 3.0.0

Major Changes

  • Most users should no longer import directly from lit-element, and instead prefer importing LitElement from the lit packages. The default entry point for lit-element remains backward-compatible and includes all decorators. However, it's recommended to use import {LitElement} from 'lit'; and import decorators from lit/decorators as necessary. See the Upgrade Guide for more details.
  • UpdatingElement has been moved from the lit-element package to the @lit/reactive-element package and renamed to ReactiveElement. See the ReactiveElement API documentation for more details. In addition, the source for css-tag, and all decorators have been moved to @lit/reactive-element. However, all symbols are re-exported from both lit and lit-element packages.
  • The @internalProperty decorator has been renamed to @state.
  • Errors that occur during the update cycle were previously squelched to allow subsequent updates to proceed normally. Now errors are re-fired asynchronously so they can be detected. Errors can be observed via an unhandledrejection event handler on window.
  • The lib folder has been removed.
  • Rendering of renderRoot/shadowRoot) via createRenderRoot and support for static styles has moved from LitElement to ReactiveElement.
  • The createRenderRoot method is now called just before the first update rather than in the constructor. Element code can not assume the renderRoot exists before the element hasUpdated. This change was made for compatibility with SSR.
  • ReactiveElement's initialize method has been removed. This work is now done in the element constructor.
  • The static render has been removed.
  • For consistency, renamed _getUpdateComplete to getUpdateComplete.
  • When a property declaration is reflect: true and its toAttribute function returns undefined the attribute is now removed where previously it was left unchanged (#872).
  • The dirty check in attributeChangedCallback has been removed. While technically breaking, in practice it should very rarely be (#699).
  • LitElement's adoptStyles method has been removed. Styling is now adopted in createRenderRoot. This method may be overridden to customize this behavior.
  • LitElement's static getStyles method has been renamed to static finalizeStyles and now takes a list of styles the user provided and returns the styles which should be used in the element. If this method is overridden to integrate into a style management system, typically the super implementation should be called.
  • Removed build support for TypeScript 3.4.
  • Decorators are no longer exported from the lit-element module. Instead, import any decorators you use from lit/decorators/*.
  • lit-html has been updated to 2.x.
  • Support for running in older browsers has been removed from the default configuration. Import the platform-support module to support Shady DOM. Note also that Lit parts inside <style> elements are no longer supported. See Polyfills for more details.
  • For simplicity, requestUpdate no longer returns a Promise. Instead await the updateComplete Promise.
  • Removed requestUpdateInternal. The requestUpdate method is now identical to this method and should be used instead.
  • #2103 15a8356d - Updates the exports field of package.json files to replace the subpath folder mapping syntax with an explicit list of all exported files.

    The /-suffixed syntax for subpath folder mapping originally used in these files is deprecated. Rather than update to the new syntax, this change replaces these mappings with individual entries for all exported files so that (a) users must import using extensions and (b) bundlers or other tools that don't resolve subpath folder mapping exactly as Node.js does won't break these packages' expectations around how they're imported.

Minor Changes

  • A public renderOptions class field now exists on LitElement and can be set/overridden to modify the options passed to lit-html.
  • Adds static shadowRootOptions for customizing shadowRoot options. Rather than implementing createRenderRoot, this property can be set. For example, to create a closed shadowRoot using delegates focus: static shadowRootOptions = {mode: 'closed', delegatesFocus: true}.
  • Adds development mode, which can be enabled by setting the development Node exports condition. See Development and production builds for more details.

Patch Changes

  • #1964 f43b811 - Don't publish src/ to npm.
  • For efficiency, the css function now maintains a cache and will use a cached value if available when the same style text is requested.
  • Fixed reflecting a property when it is set in a setter of another property that is called because its attribute changed (#965).
  • Fixed exceptions when parsing attributes from JSON (#722).
  • Fixed issue with combining static get properties on an undefined superclass with @property on a subclass ([#890]https://github.com/Polymer/lit-element/issues/890));

@lit/reactive-element - 1.0.0

Major Changes

  • @lit/reactive-element is a new package that factors out the base class that provides the reactive update lifecycle based on property/attribute changes to LitElement (what was previously called UpdatingElement) into a separate package. LitElement now extends ReactiveElement to add lit-html rendering via the render() callback. See ReactiveElement API for more details.
  • UpdatingElement has been renamed to ReactiveElement.
  • The updating-element package has been renamed to @lit/reactive-element.
  • The @internalProperty decorator has been renamed to @state.
  • For consistency, renamed _getUpdateComplete to getUpdateComplete.
  • When a property declaration is reflect: true and its toAttribute function returns undefined the attribute is now removed where previously it was left unchanged (#872).
  • Errors that occur during the update cycle were previously squelched to allow subsequent updates to proceed normally. Now errors are re-fired asynchronously so they can be detected. Errors can be observed via an unhandledrejection event handler on window.
  • ReactiveElement's renderRoot is now created when the element's connectedCallback is initially run.
  • Removed requestUpdateInternal. The requestUpdate method is now identical to this method and should be used instead.
  • The initialize method has been removed. This work is now done in the element constructor.

Minor Changes

  • Adds static addInitializer for adding a function which is called with the element instance when is created. This can be used, for example, to create decorators which hook into element lifecycle by creating a reactive controller (#1663).
  • Added ability to add a controller to an element. A controller can implement callbacks that tie into element lifecycle, including connectedCallback, disconnectedCallback, willUpdate, update, and updated. To ensure it has access to the element lifecycle, a controller should be added in the element's constructor. To add a controller to the element, call addController(controller).
  • Added removeController(controller) which can be used to remove a controller from a ReactiveElement.
  • Added willUpdate(changedProperties) lifecycle method to UpdatingElement. This is called before the update method and can be used to compute derived state needed for updating. This method is intended to be called during server side rendering and should not manipulate element DOM.

lit-html - 2.0.0

Major Changes

  • The templateFactory option of RenderOptions has been removed.
  • TemplateProcessor has been removed.
  • Symbols are not converted to a string before mutating DOM, so passing a Symbol to an attribute or text binding will result in an exception.
  • The shady-render module has been removed and is now part of platform-support, and Lit's polyfill support now adds the following limitations: (1) Bindings in style elements are no longer supported. Previously these could not change and in the future they may be supported via static bindings. (2) ShadyCSS.styleElement is no longer called automatically. This must be called whenever dynamic changes that affect styling are made that involve css custom property shimming (older browsers) or changes to custom properties used via the deprecated @apply feature. It was previously called only on first render, and it is now up to the user to decide when this should be called. See Polyfills for more details.
  • render() no longer clears the container it's rendered to. It now appends to the container by default.
  • Expressions in comments are no longer rendered or updated. See Valid expression locations for more details.
  • Template caching happens per callsite, not per template-tag/callsize pair. This means some rare forms of highly dynamic template tags are no longer supported.
  • Arrays and other iterables passed to attribute bindings are not specially handled. Arrays will be rendered with their default toString representation. This means that html`<div class=${['a', 'b']}> will render <div class="a,b"> instead of <div class="a b">. To get the old behavior, use array.join(' ').
  • Multiple bindings in a single attribute value don't require the attribute value is quoted, as long as there is no whitespace or other attribute-ending character in the attribute value. html`<div id=${a}-${b}>`
  • The directive and part APIs are significantly different. See Custom Directives and the Upgrade Guide for more details.
  • The Directive base class and directive() factory function are now exported from the lit-html/directive.js module.
  • NodePart has been renamed to ChildPart, along with other methods and variables that use the "Node" naming, like PartType.Node which is now PartType.CHILD.
  • The part exports (ChildPart, AttributePart, etc) have been change to interface-only exports. The constructors are no longer exported. Directive authors should use helpers in directive-helpers.js to construct parts.
  • The eventContext render option has been changed to host.
  • #2103 15a8356d - Updates the exports field of package.json files to replace the subpath folder mapping syntax with an explicit list of all exported files.

    The /-suffixed syntax for subpath folder mapping originally used in these files is deprecated. Rather than update to the new syntax, this change replaces these mappings with individual entries for all exported files so that (a) users must import using extensions and (b) bundlers or other tools that don't resolve subpath folder mapping exactly as Node.js does won't break these packages' expectations around how they're imported.

  • #1764 0b4d6eda - Don't allow classMap to remove static classes. This keeps classMap consistent with building a string out of the classnames to be applied.

Minor Changes

  • Added renderBefore to render options. If specified, content is rendered before the node given via render options, e.g. {renderBefore: node}.
  • Added development mode, which can be enabled by setting the development Node exports condition. See Development and production builds for more details.
  • All usage of instanceof has been removed, making rendering more likely to work when multiple instances of the library interact.
  • Template processing is more robust to expressions in places other than text and attribute values.
  • render now returns the ChildPart that was created/updated by render.
  • Added AsyncDirective, which is a Directive subclass whose disconnected callback will be called when the part containing the directive is cleared (or transitively cleared by a Part higher in the tree) or manually disconnected using the setConnected API, and whose reconnected callback will be called when manually re-connected using setConnected. When implementing disconnected, the reconnected callback should also be implemented to return the directive to a usable state. Note that LitElement will disconnect directives upon element disconnection, and re-connect directives upon element re-connection. See Async directives for more details.
  • Added setConnected(isConnected: boolean) to ChildPart; when called with false, the disconnected callback will be run on any directives contained within the part (directly or transitively), but without clearing or causing a re-render to the tree. When called with true, any such directives' reconnected callback will be called prior to its next update/render callbacks. Note that LitElement will call this method by default on the rendered part in its connectedCallback and disconnectedCallback.
  • Added the static-html module, a static html tag function, a literal tag function, and unsafeStatic(), which allows template authors to add strings to the static structure of the template, before it's parsed as HTML. See Static expressions for more details.
  • Added lit-html/directive-helpers.js module with helpers for creating custom directives. See Custom directives for more details.
  • Rendering null, undefined, or empty string in a ChildPart now has the same affect as rendering nothing: it does not produce an empty text node. When rendering into an element with Shadow DOM, this makes it harder to inadvertently prevent <slot> fallback content from rendering.
  • Nested directives whose parent returns noChange are now unchanged. This allows the guard directive to guard directive values (#1519).
  • Added optional creationScope to RenderOptions, which controls the node from which the template is cloned from.
  • Added support for running with Trusted Types enforced.

Patch Changes

  • #1922 8189f094 - Binding noChange into an interpolated attribute expression now no longer removes the attribute on first render - instead it acts like an empty string. This is mostly noticable when using until() without a fallback in interpolated attributes.

  • #1964 f43b811 - Don't publish src/ to npm.

  • #2070 a48f39c8 - Throw instead of rendering an innocuous value into a style or script when security hooks are enabled.

  • #2044 662209c3 - Improves disconnection handling for first-party AsyncDirectives (until, asyncAppend, asyncReplace) so that the directive (and any DOM associated with it) can be garbage collected before any promises they are awaiting resolve.