From 226e0ee5c5862f9e07dcd36749d086a3e8ef31e9 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Mon, 6 Jan 2020 17:37:53 +0100 Subject: [PATCH 01/15] Initial draft for server runtime proposal --- text/0112-server-runtime.md | 331 ++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 text/0112-server-runtime.md diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md new file mode 100644 index 00000000..834ff219 --- /dev/null +++ b/text/0112-server-runtime.md @@ -0,0 +1,331 @@ +--- +title: LWC server runtime +status: DRAFTED +created_at: 2020-01-06 +updated_at: 2020-01-06 +pr: (leave this empty until the PR is created) +--- + +# LWC server runtime + +## Summary + +The LWC engine has been designed from the beginning to run in an environment with access to the DOM APIs. To accommodate server-side rendering (SSR) requirements, we need a way to decouple the engine from the DOM APIs. The existing `@lwc/engine` will be replaced by 3 new packages: +- `@lwc/engine-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. +- `@lwc/engine-dom` exposes LWC APIs available on the browser. +- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. + +## Scope + +LWC SSR support is a broad subject, this proposal only focusses on a subset of it. This proposal covers the following topics: + +- How to evaluate the existing LWC engine module in a javascript context without DOM access. +- What LWC APIs should be exposed in both environments. +- How the LWC engine share the same logic when evaluated in both environments. + +The following topics are considered out of the scope of this proposal: + +- How the LWC component tree rendered on the server gets serialized and sent to the browser. +- How the serialized component tree gets rehydrated when processed by the browser. + +## Motivation + +The first step in enabling SSR support on LWC is to be able to run the engine in a non-DOM-enabled environment. It is currently impossible to evaluate the engine in such environment due to its hard dependencies on DOM APIs. + +While we want to share as much logic as possible between the different environments and make SSR as transparent as possible for component authors, the runtime behavior and APIs exposed by the engine greatly differs between environments. In this regard, it makes sense to break up the current `@lwc/engine` package into multiple packages specifically tailored for each environment. + +Distributing multiple versions of the LWC engine offers the capability to expose different APIs depending on the execution context. On one hand, the current implementation of `createElement` with reaction hooks doesn't make sense in Node.js since there is no DOM tree to attach the created element. In the same way, the `renderToString` method doesn't make sense to ship in a browser. + +## Detailed design + +The existing `@lwc/engine` will be replaced by 3 new packages: + +- `@lwc/engine-core`: Core logic shared by the different runtime including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. The only provide APIs for building custom renderers. +- `@lwc/engine-dom`: Runtime that can be used to render LWC component trees in a DOM environment. This package is built on top of `@lwc/engine-core`. +- `@lwc/engine-server`: Runtime used that can be used to render LWC component trees as strings. This package is built on top of `@lwc/engine-core`. + +The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience should be identical. + +**`c/app/app.js`:** + +```js +import { LightningElement } from "lwc"; + +export default class App extends LightningElement {} +``` + +**`client.js`:** + +```js +import { createElement } from "lwc"; // Aliased to @lwc/engine-dom +import App from "c/app"; + +const app = createElement("c-app", { is: App }); +document.body.appendChild(app); +``` + +**`server.js`:** + +```js +import { createElement, renderToString } from "lwc"; // Aliased to @lwc/engine-server +import App from "c/app"; + +const app = createElement("c-app", { is: App }); +const str = renderToString(app); + +console.log(str); +``` + +### `@lwc/engine-core` + +This packages exposes the following platform-agnostic APIs: + +- `LightningElement` +- `api` +- `track` +- `readonly` +- `wire` +- `setFeatureFlag` +- `getComponentDef` +- `isComponentConstructor` +- `getComponentConstructor` +- `unwrap` +- `registerTemplate` +- `registerComponent` +- `registerDecorators` +- `sanitizeAttribute` + +The DOM APIs used by the rendered engine are injected by the runtime depending on the environment. A list of all the DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage) section in the Appendix. + +### `@lwc/engine-dom` + +This package exposes the following APIs: + +- `createElement` + reaction hooks +- `buildCustomElementConstructor` +- `isNodeFromTemplate` + +This package injects the native DOM APIs into the `@lwc/engine-core` rendering engine. + +### `@lwc/engine-server` + +This package exposes the following APIs: + +- `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. +- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serialize it to string. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. + +This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produces a lightweight DOM structure that can be serialized into a string by the `renderToString` method. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. + +## Drawbacks + +- Requires a complete rewrite of the current `@lwc/engine` +- Might require substantial changes to the existing tools (jest preset, rollup plugins, ...) to load the right engine depending on the environment. + +## Alternatives + +### Using a DOM implementation in JavaScript before evaluating the engine + +Prior the evaluation of the LWC engine, we would evaluate a DOM implementation write in JavaScript (like [jsdom](https://github.com/jsdom/jsdom) or [domino](https://github.com/fgnass/domino)). The compelling aspect of this approach is that it requires almost no change to the engine to work. + +There are multiple drawbacks with this approach: + +- Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance. +- Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server. + +## Adoption strategy + +TBD + +## How we teach this + +TBD + +## Unresolved questions + +**How to load the right version of the engine when resolving the `lwc` identifier?** + +For example, when running in Node.js `lwc` might resolve to `@lwc/engine-server` for SSR purposes, but also to `@lwc/engine-dom` when running test via jest and jsdom. + +One way to solve this would be to import the platform-specific APIs from the appropriate module: `createElement` from `@lwc/engine-dom`, `createElement` and `renderToString` from `@lwc/engine-server` and use. Platform-agnostic APIs should be imported from `@lwc` (which becomes an alias to `@lwc/engine-core`). + +--- + +## Appendix + +### DOM APIs usage + +We can break-down the current LWC DOM usages into 3 different categories: + +- [DOM constructors used by the engine](#dom-constructors-used-by-the-engine) +- [DOM methods and accessors used by the engine](#dom-methods-and-accessors-used-by-the-engine) +- [DOM constructors used by component authors](#dom-constructors-used-by-component-authors) + +#### DOM constructors used by the engine + +The engine currently relies on the following DOM constructors during evaluation and at runtime: + +##### `HTMLElement` + +- **DOM usage:** Used by the engine to extract the descriptors and reassign them to the `LightningElement` prototype. +- **SSR usage:** 🔵 NOT REQUIRED +- **Observations:** We can create a hard-coded list of all the needed accessors: HTML global attributes, aria reflection properties and HTML the good part. + +##### `ShadowRoot` + +- **DOM usage:** Used by the engine to traverse the DOM tree to find the error boundary and create the component call stack. +- **SSR usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** Framework implementing the error boundary concept (like React or Vue), use equivalent of the LWC `VM.owner` back-pointer to traverse the component tree up instead of relying on the DOM tree. This discussion might require another RFC. + +#### DOM methods and accessors used by the engine + +On top of this the engine also rely on the following DOM methods and accessors: + +##### `EventTarget.prototype.dispatchEvent()` + +- **DOM usage:** Exposed via `LightningElement.prototype.dispatchEvent()` +- **SSR usage:** 🔴 REQUIRED +- **Observations:** Components may dispatch event once connected. + +##### `EventTarget.prototype.addEventListener()` + +- **DOM usage:** Exposed via `LightningElement.prototype.addEventListener()`. Used by the rendering engine to handle `on*` directive from the template. +- **SSR usage:** 🔴 REQUIRED + +##### `EventTarget.prototype.removeEventListener()` + +- **DOM usage:** Exposed via `LightningElement.prototype.removeElementListener()`. +- **SSR usage:** 🔴 REQUIRED + +##### `Node.prototype.appendChild()` + +- **DOM usage:** Used by the upgrade mechanism, synthetic shadow styling and to enforce restrictions. +- **SSR usage:** 🔵 NOT REQUIRED +- **Observations:** This API can be replaced by `Node.prototype.insertBefore(elm, null)`. + +##### `Node.prototype.insertBefore()` + +- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. +- **SSR Usage:** 🔴 REQUIRED + +##### `Node.prototype.removeChild()` + +- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. +- **SSR Usage:** 🔴 REQUIRED + +##### `Node.prototype.replaceChild()` + +- **DOM usage:** Used by the upgrade mechanism, and to enforce restrictions +- **SSR Usage:** 🔴 REQUIRED + +##### `Node.prototype.parentNode` (getter) + +- **DOM usage:** Used to traverse the DOM tree +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** Depending on how the error boundary is implemented, we might be able to get rid of this API. + +##### `Element.prototype.hasAttribute()` + +- **DOM usage:** Used by the aria reflection polyfill +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** For SSR, we will not need this polyfill + +##### `Element.prototype.getAttribute()` + +- **DOM usage:** Exposed via `LightningElement.prototype.getAttribute()`. Used by the aria reflection polyfill +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.getAttributeNS(null, name)` to reduce the API surface needed by the engine. + +##### `Element.prototype.getAttributeNS()` + +- **DOM usage:** Exposed via `LightningElement.prototype.getAttributeNS()` +- **SSR Usage:** 🔴 REQUIRED + +##### `Element.prototype.setAttribute()` + +- **DOM usage:** Exposed via `LightningElement.prototype.setAttribute()`. Used by the rendering engine, for synthetic shadow styling and by the aria reflection polyfill. +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.setAttributeNS(null, name)` to reduce the API surface needed by the engine. + +##### `Element.prototype.setAttributeNS()` + +- **DOM usage:** Exposed via `LightningElement.prototype.setAttributeNS()`. Used by the rendering engine. +- **SSR Usage:** 🔴 REQUIRED + +##### `Element.prototype.removeAttribute()` + +- **DOM usage:** Exposed via `LightningElement.prototype.removeAttribute()`. Used by the rendering engine, for the synthetic shadow styling and by the aria reflection polyfill +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.removeAttributeNS(null, name)` to reduce the API surface needed by the engine. + +##### `Element.prototype.removeAttributeNS()` + +- **DOM usage:** Exposed via `LightningElement.prototype.removeAttributeNS()`. +- **SSR Usage:** 🔴 REQUIRED + +##### `Element.prototype.getElementsByTagName()` + +- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByTagName()`. + +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. + +##### `Element.prototype.getElementsByClassName()` + +- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByClassName()`. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. + +##### `Element.prototype.querySelector()` + +- **DOM usage:** Exposed via `LightningElement.prototype.querySelector()`. Used to enforce restrictions. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. + +##### `Element.prototype.querySelectorAll()` + +- **DOM usage:** Exposed via `LightningElement.prototype.querySelectorAll()`. Used to enforce restrictions. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. + +##### `Element.prototype.getBoundingClientRect()` + +- **DOM usage:** Exposed via `LightningElement.prototype.getBoundingClientRect()`. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** Running a layout engine during SSR, is a complex task that will not bring much values to component authors. Returning an empty `DOMRect` (where all the properties are set to `0`), might be the best approach here. + +##### `Element.prototype.classList` (getter) + +- **DOM usage:** Exposed via `LightningElement.prototype.classList`. Used by the rendering engine. +- **SSR Usage:** 🔴 REQUIRED + +##### `ShadowRoot.prototype.host` (getter) + +- **DOM usage:** Used to traverse the DOM. +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** Depending on how the error boundary is implemented, we might be able to get rid of this API. + +##### `ShadowRoot.prototype.innerHTML` (setter) + +- **DOM usage:** Used to reset the shadow root. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This is a performance optimization to quickly reset the ShadowRoot. We can use `Node.prototype.removeChild` as a replacement. + +##### HTML global attributes (setters and getters) and Aria reflection properties + +- **DOM usage:** Exposed on the `LightningElement.prototype` Used by the rendering to set the properties on custom elements. +- **SSR Usage:** 🔴 REQUIRED + +#### DOM constructors used by component authors + +Finally, on top of all the APIs used by the engine to evaluate and run, component authors need to have access to the following DOM constructors in the context of SSR. + +##### `CustomEvent` + +- **DOM usage:** Used to dispatch events +- **SSR Usage:** 🔴 REQUIRED + +##### `Event` + +- **DOM usage:** Used to dispatch events +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** This might not be needed because `CustomEvent` inherits from `Event` and because `CustomEvent` is the recommended way to dispatch non-standard events. From 748a64146ef35a6c26faef6fcf4420ed31ed2f74 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 8 Jan 2020 14:21:06 +0100 Subject: [PATCH 02/15] Update based on feedback --- text/0112-server-runtime.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 834ff219..53f4636f 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -17,7 +17,7 @@ The LWC engine has been designed from the beginning to run in an environment wit ## Scope -LWC SSR support is a broad subject, this proposal only focusses on a subset of it. This proposal covers the following topics: +LWC SSR support is a broad subject, this proposal only focuses on a subset of it. This proposal covers the following topics: - How to evaluate the existing LWC engine module in a javascript context without DOM access. - What LWC APIs should be exposed in both environments. @@ -25,7 +25,7 @@ LWC SSR support is a broad subject, this proposal only focusses on a subset of i The following topics are considered out of the scope of this proposal: -- How the LWC component tree rendered on the server gets serialized and sent to the browser. +- How the LWC component tree rendered on the server gets serialized to HTML and sent to the browser. - How the serialized component tree gets rehydrated when processed by the browser. ## Motivation @@ -40,7 +40,7 @@ Distributing multiple versions of the LWC engine offers the capability to expose The existing `@lwc/engine` will be replaced by 3 new packages: -- `@lwc/engine-core`: Core logic shared by the different runtime including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. The only provide APIs for building custom renderers. +- `@lwc/engine-core`: Core logic shared by the different runtimes including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. It only provides APIs for building custom runtimes. - `@lwc/engine-dom`: Runtime that can be used to render LWC component trees in a DOM environment. This package is built on top of `@lwc/engine-core`. - `@lwc/engine-server`: Runtime used that can be used to render LWC component trees as strings. This package is built on top of `@lwc/engine-core`. From bb1efd30c230db70e99f1a11bcb6f2b30a2e6b25 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Mon, 13 Jan 2020 15:43:50 +0100 Subject: [PATCH 03/15] incorporate feedback --- text/0112-server-runtime.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 53f4636f..38ebd555 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -112,14 +112,14 @@ This package injects the native DOM APIs into the `@lwc/engine-core` rendering e This package exposes the following APIs: - `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. -- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serialize it to string. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. +- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serializes it to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. -This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produces a lightweight DOM structure that can be serialized into a string by the `renderToString` method. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. +This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. ## Drawbacks - Requires a complete rewrite of the current `@lwc/engine` -- Might require substantial changes to the existing tools (jest preset, rollup plugins, ...) to load the right engine depending on the environment. +- Might require substantial changes to the existing tools (jest preset, rollup plugins, etc.) to load the right engine depending on the environment. ## Alternatives From b873783091f2440e6022c455bc7f3806e369adf3 Mon Sep 17 00:00:00 2001 From: Diego Ferreiro Val Date: Fri, 17 Jan 2020 08:44:16 -0800 Subject: [PATCH 04/15] Update 0112-server-runtime.md --- text/0112-server-runtime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 38ebd555..ffc176f9 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -3,7 +3,7 @@ title: LWC server runtime status: DRAFTED created_at: 2020-01-06 updated_at: 2020-01-06 -pr: (leave this empty until the PR is created) +pr: https://github.com/salesforce/lwc-rfcs/pull/23 --- # LWC server runtime From ccbd9f38a7ff30d2bd604107e6eb13608366aed2 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Fri, 31 Jan 2020 06:50:59 -0800 Subject: [PATCH 05/15] Apply PR feedback Co-Authored-By: Eugene Kashida --- text/0112-server-runtime.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index ffc176f9..9851240c 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -42,7 +42,7 @@ The existing `@lwc/engine` will be replaced by 3 new packages: - `@lwc/engine-core`: Core logic shared by the different runtimes including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. It only provides APIs for building custom runtimes. - `@lwc/engine-dom`: Runtime that can be used to render LWC component trees in a DOM environment. This package is built on top of `@lwc/engine-core`. -- `@lwc/engine-server`: Runtime used that can be used to render LWC component trees as strings. This package is built on top of `@lwc/engine-core`. +- `@lwc/engine-server`: Runtime that can be used to render LWC component trees as strings. This package is built on top of `@lwc/engine-core`. The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience should be identical. @@ -112,7 +112,7 @@ This package injects the native DOM APIs into the `@lwc/engine-core` rendering e This package exposes the following APIs: - `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. -- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serializes it to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. +- `renderToString(element: ServerHTMLElement): string`: This method synchronously serializes an LWC component tree to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. @@ -146,7 +146,7 @@ TBD For example, when running in Node.js `lwc` might resolve to `@lwc/engine-server` for SSR purposes, but also to `@lwc/engine-dom` when running test via jest and jsdom. -One way to solve this would be to import the platform-specific APIs from the appropriate module: `createElement` from `@lwc/engine-dom`, `createElement` and `renderToString` from `@lwc/engine-server` and use. Platform-agnostic APIs should be imported from `@lwc` (which becomes an alias to `@lwc/engine-core`). +One way to solve this would be to import the platform-specific APIs from the appropriate module: `createElement` from `@lwc/engine-dom` and `createElement` and `renderToString` from `@lwc/engine-server`. Platform-agnostic APIs should be imported from `lwc` (which becomes an alias to `@lwc/engine-core`). --- From e091f23de446ae0ab31270d3e3f17f89dee8ad06 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Fri, 10 Apr 2020 16:00:59 +0200 Subject: [PATCH 06/15] 04/09 meeting feedback --- text/0112-server-runtime.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 9851240c..9adba2cd 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -57,7 +57,7 @@ export default class App extends LightningElement {} **`client.js`:** ```js -import { createElement } from "lwc"; // Aliased to @lwc/engine-dom +import { createElement } from "@lwc/engine-dom"; import App from "c/app"; const app = createElement("c-app", { is: App }); @@ -67,7 +67,7 @@ document.body.appendChild(app); **`server.js`:** ```js -import { createElement, renderToString } from "lwc"; // Aliased to @lwc/engine-server +import { createElement, renderToString } from "@lwc/engine-server"; import App from "c/app"; const app = createElement("c-app", { is: App }); @@ -102,19 +102,24 @@ The DOM APIs used by the rendered engine are injected by the runtime depending o This package exposes the following APIs: - `createElement` + reaction hooks -- `buildCustomElementConstructor` - `isNodeFromTemplate` This package injects the native DOM APIs into the `@lwc/engine-core` rendering engine. ### `@lwc/engine-server` -This package exposes the following APIs: +This package exposes the following API: + +- `renderComponent(name: string, ctor: typeof LightningElement, props?: Record): string`: This method creates an renders an LWC component synchronously to a string. + +When a component is rendered using `renderComponent`, the following restriction applies: + - each created component will execute the following life-cycle hooks once `constructor`, `connectedCallback` and `render` + - properties and methods annotated with `@wire` will not be invoked + - component reactivity is disabled -- `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. -- `renderToString(element: ServerHTMLElement): string`: This method synchronously serializes an LWC component tree to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. +Another API might be created to accommodate wire adapter invocation and asynchronous data fetching on the server. -This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. +This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent` and only implements a restricted subset of the event dispatching algorithm on the server (no bubbling, no event retargeting). ## Drawbacks @@ -132,21 +137,14 @@ There are multiple drawbacks with this approach: - Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance. - Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server. -## Adoption strategy - -TBD - ## How we teach this -TBD - -## Unresolved questions +This proposal breaks up the existing `@lwc/engine` package into multiple packages. Because of this APIs that used to be imported from `lwc` might not be present anymore. With this proposal, the `createElement` API will be moving to `@lwc/engine-dom`. This change constitutes a breaking change by itself, so we need to be careful how this change will be released. The good news is that `createElement` is considered as an experimental API and should only used to create the application root and to test components. -**How to load the right version of the engine when resolving the `lwc` identifier?** +Changing the application root creation consists in a one line change. Therefor it should be pretty straightforward updated as long as it is well documented. Updating all the usages of `createElement` in test will probably require more time. For testing purposes, the `lwc` module will be mapped to the `@lwc/engine-dom` instead of `@lwc/engine-core` for now. We will also add warning messages in the console to promulgate the new pattern. A codemod for test files can also be used rename the `lwc` import in the test files to `@lwc/engine-dom`. -For example, when running in Node.js `lwc` might resolve to `@lwc/engine-server` for SSR purposes, but also to `@lwc/engine-dom` when running test via jest and jsdom. -One way to solve this would be to import the platform-specific APIs from the appropriate module: `createElement` from `@lwc/engine-dom` and `createElement` and `renderToString` from `@lwc/engine-server`. Platform-agnostic APIs should be imported from `lwc` (which becomes an alias to `@lwc/engine-core`). +Updating the documentation for the newly added server only APIs should be enough. --- From cce06987f519315dc1dfc1e4dd1e70ce8287a3f7 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 4 Jun 2020 16:34:43 +0200 Subject: [PATCH 07/15] Goals and scope updates --- text/0112-server-runtime.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 9adba2cd..18a88d83 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -15,26 +15,25 @@ The LWC engine has been designed from the beginning to run in an environment wit - `@lwc/engine-dom` exposes LWC APIs available on the browser. - `@lwc/engine-server` exposes LWC APIs used for server-side rendering. -## Scope +>> TODO: Add more details -LWC SSR support is a broad subject, this proposal only focuses on a subset of it. This proposal covers the following topics: +## Goals -- How to evaluate the existing LWC engine module in a javascript context without DOM access. -- What LWC APIs should be exposed in both environments. -- How the LWC engine share the same logic when evaluated in both environments. +Server Side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static website, … -The following topics are considered out of the scope of this proposal: +Due to the size and complexity of this feature, the rollout of SSR is broken up in 3 different phases and is spread over multiple releases: +- **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). +- **Phase 2:** Settle on a HTML serialization format +- **Phase 3:** Rehydrate in a browser the DOM tree resulting of the serialization into an LWC component tree -- How the LWC component tree rendered on the server gets serialized to HTML and sent to the browser. -- How the serialized component tree gets rehydrated when processed by the browser. +As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component stand point, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms. -## Motivation +## Proposal Scope -The first step in enabling SSR support on LWC is to be able to run the engine in a non-DOM-enabled environment. It is currently impossible to evaluate the engine in such environment due to its hard dependencies on DOM APIs. +This proposal will be only focussing on the phase 1 of the SSR project. The phase 2 and 3 will be covered in subsequent proposals. This proposal cover 2 main aspects of the LWC SSR: -While we want to share as much logic as possible between the different environments and make SSR as transparent as possible for component authors, the runtime behavior and APIs exposed by the engine greatly differs between environments. In this regard, it makes sense to break up the current `@lwc/engine` package into multiple packages specifically tailored for each environment. - -Distributing multiple versions of the LWC engine offers the capability to expose different APIs depending on the execution context. On one hand, the current implementation of `createElement` with reaction hooks doesn't make sense in Node.js since there is no DOM tree to attach the created element. In the same way, the `renderToString` method doesn't make sense to ship in a browser. +- What are the differences between the DOM version and the SSR version of LWC? +- How to create 2 different implementation of the LWC engine while keeping the core logic shared. ## Detailed design @@ -146,6 +145,11 @@ Changing the application root creation consists in a one line change. Therefor i Updating the documentation for the newly added server only APIs should be enough. +## Unresolved questions + +* **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring the type safety. +* **How to implement LWC Context for SSR?** Context uses eventing for registration between the provider and the consumer. Since `@lwc/engine-server` will only implement a subset of the DOM eventing, we will need to evaluate how we can replace the current registration mechanism. + --- ## Appendix From c266a5a7259d01630ad16fe0e5fb1081b874a2f6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Fri, 5 Jun 2020 11:12:52 +0200 Subject: [PATCH 08/15] More restructuring --- text/0112-server-runtime.md | 507 +++++++++++++++++++++++++----------- 1 file changed, 352 insertions(+), 155 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 18a88d83..1c02e52b 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -10,21 +10,23 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Summary -The LWC engine has been designed from the beginning to run in an environment with access to the DOM APIs. To accommodate server-side rendering (SSR) requirements, we need a way to decouple the engine from the DOM APIs. The existing `@lwc/engine` will be replaced by 3 new packages: -- `@lwc/engine-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. -- `@lwc/engine-dom` exposes LWC APIs available on the browser. -- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. +The LWC engine has been designed from the beginning to run in an environment with access to the DOM APIs. To accommodate server-side rendering (SSR) requirements, we need a way to decouple the engine from the DOM APIs. The existing `@lwc/engine` will be replaced by 3 new packages: ->> TODO: Add more details +- `@lwc/engine-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. +- `@lwc/engine-dom` exposes LWC APIs available on the browser. +- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. + +> > TODO: Add more details ## Goals Server Side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static website, … Due to the size and complexity of this feature, the rollout of SSR is broken up in 3 different phases and is spread over multiple releases: -- **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). -- **Phase 2:** Settle on a HTML serialization format -- **Phase 3:** Rehydrate in a browser the DOM tree resulting of the serialization into an LWC component tree + +- **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). +- **Phase 2:** Settle on a HTML serialization format +- **Phase 3:** Rehydrate in a browser the DOM tree resulting of the serialization into an LWC component tree As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component stand point, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms. @@ -32,98 +34,258 @@ As part of phase 1, the goal is to provide a capability for LWC to render compon This proposal will be only focussing on the phase 1 of the SSR project. The phase 2 and 3 will be covered in subsequent proposals. This proposal cover 2 main aspects of the LWC SSR: -- What are the differences between the DOM version and the SSR version of LWC? -- How to create 2 different implementation of the LWC engine while keeping the core logic shared. +- What are the differences between the DOM version and the SSR version of LWC? +- How to create 2 different implementation of the LWC engine while keeping the core logic shared. ## Detailed design -The existing `@lwc/engine` will be replaced by 3 new packages: +### Approaching SSR in UI framework -- `@lwc/engine-core`: Core logic shared by the different runtimes including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. It only provides APIs for building custom runtimes. -- `@lwc/engine-dom`: Runtime that can be used to render LWC component trees in a DOM environment. This package is built on top of `@lwc/engine-core`. -- `@lwc/engine-server`: Runtime that can be used to render LWC component trees as strings. This package is built on top of `@lwc/engine-core`. +Many popular UI framework already provide options to enable server side rendering. There is no single way to implement SSR in a UI framework and each framework approach this in a different way. That being said all the major frameworks converge around 2 main approaches. -The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience should be identical. +The first approach is to **polyfill the server environment with DOM APIs and to reuse same core UI framework code on the server and on the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr). -**`c/app/app.js`:** +This approach is really convenient because it require almost no change to the core UI framework to enable SSR. Since most of the DOM APIs used by UI framework are low level dom API, most of them are easy to polyfill. Popular DOM APIs implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to polyfills. -```js -import { LightningElement } from "lwc"; +This approach also suffers from multiple drawback. The main issue is that the polyfill has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers has to always double check that the newly introduced APIs are also polyfilled properly. The other main drawback, is that by applying polyfills to evaluate the engine the same DOM interfaces and methods a also from the component code (eg. `window`, `document`, ...) which gives a false senses that the component is running in the browser. -export default class App extends LightningElement {} -``` +Using the polyfilling approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC of would look something like this. -**`client.js`:** +**`index.ts`:** -```js -import { createElement } from "@lwc/engine-dom"; -import App from "c/app"; +```ts +// Evaluate fist the DOM polyfills, then the DOM engine version of the engine. Reexport all the +// exported properties from the DOM engine. +import './dom-environment'; +export * from '@lwc/engine'; +``` -const app = createElement("c-app", { is: App }); -document.body.appendChild(app); +**`dom-environment.ts`:** + +```ts +class Node { + children: Node[] = []; + + insertBefore(child: Node, anchor: Node): void { + const anchorIndex = this.children.indexOf(anchor); + + if (anchorIndex === -1) { + this.children.push(child); + } else { + this.children.splice(anchorIndex, 0, child); + } + } +} + +class Element extends Node { + tagName: string; + + constructor(tagName: string) { + super(); + this.tagName = tagName; + } +} + +const document = { + createElement(tagName: string): Element { + return new Element(tagName); + }, +}; + +const window = { + document, +}; + +// Assigning the synthetic DOM APIs to the current environment global object. +Object.assign(globalThis, { + window, + document, + Node, + Element, +}); ``` -**`server.js`:** +The second approach is to **abstract the DOM APIs and the core UI framework**. This approach is used by [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src). This approach consists in creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded server the rendering APIs are manipulating string to create the serialized version of the components on the fly. -```js -import { createElement, renderToString } from "@lwc/engine-server"; -import App from "c/app"; +When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting at runtime this indirection, it also ensure that component code don't have access to any DOM APIs when running on the server. -const app = createElement("c-app", { is: App }); -const str = renderToString(app); +The main drawback with this approach is the amount of refactor that need to happen in LWC engine code specifically. So far the LWC engine has been designed to run in JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. -console.log(str); +In order to inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. You can find below some pseudo code for each entry point. The 2 entry points uses the same `createComponent` abstraction to create an LWC component but pass a renderer specific to the targeted environment. + +**`entry-points/dom.js`:** + +```ts +import { LightningElement, createComponent } from '@lwc/engine-core'; + +const domRenderer = { + createElement(tagName: string): Element { + return document.createElement(tagName); + }, + insertBefore(node: Node, parent: Element, anchor: Node): void { + return parent.insertBefore(node, anchor); + }, +}; + +// The method dedicated to create an LWC component in a dom environment. It returns a DOM element +// that can then inserted into the document to render. +export function createElement( + tagName: string, + opts: { Ctor: typeof LightningElement }, +): Element { + const elm = document.createElement(tagName); + createComponent(elm, Ctor, domRenderer); + return elm; +} ``` -### `@lwc/engine-core` +**`entry-points/server.js`:** + +```ts +import { LightningElement, createComponent } from '@lwc/engine-core'; + +interface ServerNode { + children: ServerNode[]; +} + +interface ServerElement extends ServerNode { + tagName: string; +} + +const serverRenderer = { + createElement(tagName: string): ServerElement { + return { + tagName, + children: [], + }; + }, + insertBefore( + node: ServerNode, + parent: ServerElement, + anchor: ServerNode, + ): void { + const anchorIndex = parent.children.indexOf(anchor); + + if (anchorIndex === -1) { + parent.children.push(child); + } else { + parent.children.splice(anchorIndex, 0, child); + } + }, +}; + +// The method dedicated to create an LWC component in a server environment. It returns a string +// resulting of the serialization component tree +export function renderComponent( + tagName: string, + opts: { Ctor: typeof LightningElement }, +): string { + const elm: ServerElement = { + tagName, + children: [], + }; + + createComponent(elm, Ctor, serverRenderer); + + return serializeElement(elm); +} +``` + +**Conclusion:** After weighting the pros and cons of the 2 approaches described above, **we decided to go with the second approach when the rendering APIs are injected lazily at runtime**. + +### Splitting `@lwc/engine` + +As discussed in the previous section, an abstraction of the the underlying rendering APIs into the core framework depending on the target environment. In order to share the core UI framework between the different environment the existing `@lwc/engine` will be split into 3 packages. -This packages exposes the following platform-agnostic APIs: +#### `@lwc/engine-core` -- `LightningElement` -- `api` -- `track` -- `readonly` -- `wire` -- `setFeatureFlag` -- `getComponentDef` -- `isComponentConstructor` -- `getComponentConstructor` -- `unwrap` -- `registerTemplate` -- `registerComponent` -- `registerDecorators` -- `sanitizeAttribute` +This package contains core logic shared by the different runtime environment including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This packages exposes the following platform-agnostic public APIs: -The DOM APIs used by the rendered engine are injected by the runtime depending on the environment. A list of all the DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage) section in the Appendix. +- `LightningElement` +- `api` +- `track` +- `readonly` +- `wire` +- `setFeatureFlag` +- `getComponentDef` +- `isComponentConstructor` +- `getComponentConstructor` +- `unwrap` +- `registerTemplate` +- `registerComponent` +- `registerDecorators` +- `sanitizeAttribute` -### `@lwc/engine-dom` +On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed -This package exposes the following APIs: +- `getComponentInternalDef(Ctor: typeof LightningElement): ComponentDef`: Get the internal component definition for a given LightningElement constructor. +- `createVM(elm: HostElement, Ctor, options: { mode: 'open' | 'closed', owner: VM | null, renderer: Renderer }): VM`: Create a new View-Model (VM) associated with an LWC component. +- `connectRootElement(elm: HostElement): void`: Mount a component and trigger a rendering. +- `disconnectRootElement(elm: HostElement): void`: Unmount a component and trigger a disconnection flow. +- `getAssociatedVMIfPresent(elm: HostElement): VM | undefined`: Retrieve the VM on a given element. +- `setElementProto(elm: HostElement): void`: Patch an element prototype with the bridge element. -- `createElement` + reaction hooks -- `isNodeFromTemplate` +The DOM APIs used by the rendered engine are injected by the runtime depending on the environment. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). The [Renderer](#renderer-interface) interface will replace those direct invocation to the DOM API usages. + +#### `@lwc/engine-dom` + +Runtime that can be used to render LWC component trees in a DOM environment. This package exposes the following APIs: + +- `createElement` + reaction hooks +- `isNodeFromTemplate` This package injects the native DOM APIs into the `@lwc/engine-core` rendering engine. -### `@lwc/engine-server` +#### `@lwc/engine-server` + +Runtime that can be used to render LWC component trees as strings. This package exposes the following API: -This package exposes the following API: +- `renderComponent(name: string, ctor: typeof LightningElement, props?: Record): string`: This method creates an renders an LWC component synchronously to a string. -- `renderComponent(name: string, ctor: typeof LightningElement, props?: Record): string`: This method creates an renders an LWC component synchronously to a string. +When a component is rendered using `renderComponent`, the following restriction applies: -When a component is rendered using `renderComponent`, the following restriction applies: - - each created component will execute the following life-cycle hooks once `constructor`, `connectedCallback` and `render` - - properties and methods annotated with `@wire` will not be invoked - - component reactivity is disabled +- each created component will execute the following life-cycle hooks once `constructor`, `connectedCallback` and `render` +- properties and methods annotated with `@wire` will not be invoked +- component reactivity is disabled Another API might be created to accommodate wire adapter invocation and asynchronous data fetching on the server. This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent` and only implements a restricted subset of the event dispatching algorithm on the server (no bubbling, no event retargeting). +The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. + +**`c/app/app.js`:** + +```js +import { LightningElement } from 'lwc'; + +export default class App extends LightningElement {} +``` + +**`client.js`:** + +```js +import { createElement } from 'lwc'; // Resolves to `@lwc/engine-dom` +import App from 'c/app'; + +const app = createElement('c-app', { is: App }); +document.body.appendChild(app); +``` + +**`server.js`:** + +```js +import { renderComponent } from '@lwc/engine-server'; // Resolves to `@lwc/engine-server` +import App from 'c/app'; + +const str = renderComponent('c-app', { is: App }); +console.log(str); +``` + ## Drawbacks -- Requires a complete rewrite of the current `@lwc/engine` -- Might require substantial changes to the existing tools (jest preset, rollup plugins, etc.) to load the right engine depending on the environment. +- Requires a complete rewrite of the current `@lwc/engine` +- Might require substantial changes to the existing tools (jest preset, rollup plugins, etc.) to load the right engine depending on the environment. ## Alternatives @@ -133,8 +295,8 @@ Prior the evaluation of the LWC engine, we would evaluate a DOM implementation w There are multiple drawbacks with this approach: -- Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance. -- Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server. +- Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance. +- Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server. ## How we teach this @@ -142,13 +304,12 @@ This proposal breaks up the existing `@lwc/engine` package into multiple package Changing the application root creation consists in a one line change. Therefor it should be pretty straightforward updated as long as it is well documented. Updating all the usages of `createElement` in test will probably require more time. For testing purposes, the `lwc` module will be mapped to the `@lwc/engine-dom` instead of `@lwc/engine-core` for now. We will also add warning messages in the console to promulgate the new pattern. A codemod for test files can also be used rename the `lwc` import in the test files to `@lwc/engine-dom`. - Updating the documentation for the newly added server only APIs should be enough. ## Unresolved questions -* **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring the type safety. -* **How to implement LWC Context for SSR?** Context uses eventing for registration between the provider and the consumer. Since `@lwc/engine-server` will only implement a subset of the DOM eventing, we will need to evaluate how we can replace the current registration mechanism. +- **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring the type safety. +- **How to implement LWC Context for SSR?** Context uses eventing for registration between the provider and the consumer. Since `@lwc/engine-server` will only implement a subset of the DOM eventing, we will need to evaluate how we can replace the current registration mechanism. --- @@ -158,25 +319,17 @@ Updating the documentation for the newly added server only APIs should be enough We can break-down the current LWC DOM usages into 3 different categories: -- [DOM constructors used by the engine](#dom-constructors-used-by-the-engine) -- [DOM methods and accessors used by the engine](#dom-methods-and-accessors-used-by-the-engine) -- [DOM constructors used by component authors](#dom-constructors-used-by-component-authors) +- [DOM constructors used by the engine](#dom-constructors-used-by-the-engine) +- [DOM methods and accessors used by the engine](#dom-methods-and-accessors-used-by-the-engine) +- [DOM constructors used by component authors](#dom-constructors-used-by-component-authors) #### DOM constructors used by the engine -The engine currently relies on the following DOM constructors during evaluation and at runtime: - ##### `HTMLElement` -- **DOM usage:** Used by the engine to extract the descriptors and reassign them to the `LightningElement` prototype. -- **SSR usage:** 🔵 NOT REQUIRED -- **Observations:** We can create a hard-coded list of all the needed accessors: HTML global attributes, aria reflection properties and HTML the good part. - -##### `ShadowRoot` - -- **DOM usage:** Used by the engine to traverse the DOM tree to find the error boundary and create the component call stack. -- **SSR usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** Framework implementing the error boundary concept (like React or Vue), use equivalent of the LWC `VM.owner` back-pointer to traverse the component tree up instead of relying on the DOM tree. This discussion might require another RFC. +- **DOM usage:** Used by the engine to extract the descriptors and reassign them to the `LightningElement` prototype. +- **SSR usage:** 🔵 NOT REQUIRED +- **Observations:** We can create a hard-coded list of all the needed accessors: HTML global attributes, aria reflection properties and HTML the good part. #### DOM methods and accessors used by the engine @@ -184,138 +337,126 @@ On top of this the engine also rely on the following DOM methods and accessors: ##### `EventTarget.prototype.dispatchEvent()` -- **DOM usage:** Exposed via `LightningElement.prototype.dispatchEvent()` -- **SSR usage:** 🔴 REQUIRED -- **Observations:** Components may dispatch event once connected. +- **DOM usage:** Exposed via `LightningElement.prototype.dispatchEvent()` +- **SSR usage:** 🔴 REQUIRED +- **Observations:** Components may dispatch event once connected. ##### `EventTarget.prototype.addEventListener()` -- **DOM usage:** Exposed via `LightningElement.prototype.addEventListener()`. Used by the rendering engine to handle `on*` directive from the template. -- **SSR usage:** 🔴 REQUIRED +- **DOM usage:** Exposed via `LightningElement.prototype.addEventListener()`. Used by the rendering engine to handle `on*` directive from the template. +- **SSR usage:** 🔴 REQUIRED ##### `EventTarget.prototype.removeEventListener()` -- **DOM usage:** Exposed via `LightningElement.prototype.removeElementListener()`. -- **SSR usage:** 🔴 REQUIRED +- **DOM usage:** Exposed via `LightningElement.prototype.removeElementListener()`. +- **SSR usage:** 🔴 REQUIRED ##### `Node.prototype.appendChild()` -- **DOM usage:** Used by the upgrade mechanism, synthetic shadow styling and to enforce restrictions. -- **SSR usage:** 🔵 NOT REQUIRED -- **Observations:** This API can be replaced by `Node.prototype.insertBefore(elm, null)`. +- **DOM usage:** Used by the upgrade mechanism, synthetic shadow styling and to enforce restrictions. +- **SSR usage:** 🔵 NOT REQUIRED +- **Observations:** This API can be replaced by `Node.prototype.insertBefore(elm, null)`. ##### `Node.prototype.insertBefore()` -- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. +- **SSR Usage:** 🔴 REQUIRED ##### `Node.prototype.removeChild()` -- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions. +- **SSR Usage:** 🔴 REQUIRED ##### `Node.prototype.replaceChild()` -- **DOM usage:** Used by the upgrade mechanism, and to enforce restrictions -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Used by the upgrade mechanism, and to enforce restrictions +- **SSR Usage:** 🔴 REQUIRED ##### `Node.prototype.parentNode` (getter) -- **DOM usage:** Used to traverse the DOM tree -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** Depending on how the error boundary is implemented, we might be able to get rid of this API. +- **DOM usage:** Used to traverse the DOM tree +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** Depending on how the error boundary is implemented, we might be able to get rid of this API. ##### `Element.prototype.hasAttribute()` -- **DOM usage:** Used by the aria reflection polyfill -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** For SSR, we will not need this polyfill +- **DOM usage:** Used by the aria reflection polyfill +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** For SSR, we will not need this polyfill ##### `Element.prototype.getAttribute()` -- **DOM usage:** Exposed via `LightningElement.prototype.getAttribute()`. Used by the aria reflection polyfill -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** We might use `Element.prototype.getAttributeNS(null, name)` to reduce the API surface needed by the engine. +- **DOM usage:** Exposed via `LightningElement.prototype.getAttribute()`. Used by the aria reflection polyfill +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.getAttributeNS(null, name)` to reduce the API surface needed by the engine. ##### `Element.prototype.getAttributeNS()` -- **DOM usage:** Exposed via `LightningElement.prototype.getAttributeNS()` -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Exposed via `LightningElement.prototype.getAttributeNS()` +- **SSR Usage:** 🔴 REQUIRED ##### `Element.prototype.setAttribute()` -- **DOM usage:** Exposed via `LightningElement.prototype.setAttribute()`. Used by the rendering engine, for synthetic shadow styling and by the aria reflection polyfill. -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** We might use `Element.prototype.setAttributeNS(null, name)` to reduce the API surface needed by the engine. +- **DOM usage:** Exposed via `LightningElement.prototype.setAttribute()`. Used by the rendering engine, for synthetic shadow styling and by the aria reflection polyfill. +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.setAttributeNS(null, name)` to reduce the API surface needed by the engine. ##### `Element.prototype.setAttributeNS()` -- **DOM usage:** Exposed via `LightningElement.prototype.setAttributeNS()`. Used by the rendering engine. -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Exposed via `LightningElement.prototype.setAttributeNS()`. Used by the rendering engine. +- **SSR Usage:** 🔴 REQUIRED ##### `Element.prototype.removeAttribute()` -- **DOM usage:** Exposed via `LightningElement.prototype.removeAttribute()`. Used by the rendering engine, for the synthetic shadow styling and by the aria reflection polyfill -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** We might use `Element.prototype.removeAttributeNS(null, name)` to reduce the API surface needed by the engine. +- **DOM usage:** Exposed via `LightningElement.prototype.removeAttribute()`. Used by the rendering engine, for the synthetic shadow styling and by the aria reflection polyfill +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** We might use `Element.prototype.removeAttributeNS(null, name)` to reduce the API surface needed by the engine. ##### `Element.prototype.removeAttributeNS()` -- **DOM usage:** Exposed via `LightningElement.prototype.removeAttributeNS()`. -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Exposed via `LightningElement.prototype.removeAttributeNS()`. +- **SSR Usage:** 🔴 REQUIRED ##### `Element.prototype.getElementsByTagName()` -- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByTagName()`. +- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByTagName()`. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. ##### `Element.prototype.getElementsByClassName()` -- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByClassName()`. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. +- **DOM usage:** Exposed via `LightningElement.prototype.getElementsByClassName()`. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `HTMLCollection` when running on the server. ##### `Element.prototype.querySelector()` -- **DOM usage:** Exposed via `LightningElement.prototype.querySelector()`. Used to enforce restrictions. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. +- **DOM usage:** Exposed via `LightningElement.prototype.querySelector()`. Used to enforce restrictions. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. ##### `Element.prototype.querySelectorAll()` -- **DOM usage:** Exposed via `LightningElement.prototype.querySelectorAll()`. Used to enforce restrictions. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. +- **DOM usage:** Exposed via `LightningElement.prototype.querySelectorAll()`. Used to enforce restrictions. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** This method is only exposed to select elements from the component light DOM, which is only available from the `renderedCallback`. Since SSR doesn't run `renderedCallback`, we can always returns an empty `NodeList` when running on the server. ##### `Element.prototype.getBoundingClientRect()` -- **DOM usage:** Exposed via `LightningElement.prototype.getBoundingClientRect()`. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** Running a layout engine during SSR, is a complex task that will not bring much values to component authors. Returning an empty `DOMRect` (where all the properties are set to `0`), might be the best approach here. +- **DOM usage:** Exposed via `LightningElement.prototype.getBoundingClientRect()`. +- **SSR Usage:** 🔵 NOT REQUIRED +- **Observations:** Running a layout engine during SSR, is a complex task that will not bring much values to component authors. Returning an empty `DOMRect` (where all the properties are set to `0`), might be the best approach here. ##### `Element.prototype.classList` (getter) -- **DOM usage:** Exposed via `LightningElement.prototype.classList`. Used by the rendering engine. -- **SSR Usage:** 🔴 REQUIRED - -##### `ShadowRoot.prototype.host` (getter) - -- **DOM usage:** Used to traverse the DOM. -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** Depending on how the error boundary is implemented, we might be able to get rid of this API. - -##### `ShadowRoot.prototype.innerHTML` (setter) - -- **DOM usage:** Used to reset the shadow root. -- **SSR Usage:** 🔵 NOT REQUIRED -- **Observations:** This is a performance optimization to quickly reset the ShadowRoot. We can use `Node.prototype.removeChild` as a replacement. +- **DOM usage:** Exposed via `LightningElement.prototype.classList`. Used by the rendering engine. +- **SSR Usage:** 🔴 REQUIRED ##### HTML global attributes (setters and getters) and Aria reflection properties -- **DOM usage:** Exposed on the `LightningElement.prototype` Used by the rendering to set the properties on custom elements. -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Exposed on the `LightningElement.prototype` Used by the rendering to set the properties on custom elements. +- **SSR Usage:** 🔴 REQUIRED #### DOM constructors used by component authors @@ -323,11 +464,67 @@ Finally, on top of all the APIs used by the engine to evaluate and run, componen ##### `CustomEvent` -- **DOM usage:** Used to dispatch events -- **SSR Usage:** 🔴 REQUIRED +- **DOM usage:** Used to dispatch events +- **SSR Usage:** 🔴 REQUIRED ##### `Event` -- **DOM usage:** Used to dispatch events -- **SSR Usage:** 🔶 MIGHT BE REQUIRED -- **Observations:** This might not be needed because `CustomEvent` inherits from `Event` and because `CustomEvent` is the recommended way to dispatch non-standard events. +- **DOM usage:** Used to dispatch events +- **SSR Usage:** 🔶 MIGHT BE REQUIRED +- **Observations:** This might not be needed because `CustomEvent` inherits from `Event` and because `CustomEvent` is the recommended way to dispatch non-standard events. + +### Renderer interface + +```ts +export interface Renderer { + syntheticShadow: boolean; + insert(node: HostNode, parent: HostElement, anchor: HostNode | null): void; + remove(node: HostNode, parent: HostElement): void; + createElement(tagName: string, namespace?: string): HostElement; + createText(content: string): HostNode; + nextSibling(node: HostNode): HostNode | null; + attachShadow(element: HostElement, options: ShadowRootInit): HostNode; + setText(node: HostNode, content: string): void; + getAttribute( + element: HostElement, + name: string, + namespace?: string | null, + ): string | null; + setAttribute( + element: HostElement, + name: string, + value: string, + namespace?: string | null, + ): void; + removeAttribute( + element: HostElement, + name: string, + namespace?: string | null, + ): void; + addEventListener( + target: HostElement, + type: string, + callback: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions | boolean, + ): void; + removeEventListener( + target: HostElement, + type: string, + callback: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions | boolean, + ): void; + dispatchEvent(target: HostNode, event: Event): boolean; + getClassList(element: HostElement): DOMTokenList; + getStyleDeclaration(element: HostElement): CSSStyleDeclaration; + getBoundingClientRect(element: HostElement): ClientRect; + querySelector(element: HostElement, selectors: string): HostElement | null; + querySelectorAll(element: HostElement, selectors: string): NodeList; + getElementsByTagName( + element: HostElement, + tagNameOrWildCard: string, + ): HTMLCollection; + getElementsByClassName(element: HostElement, names: string): HTMLCollection; + isConnected(node: HostNode): boolean; + tagName(element: HostElement): string; +} +``` From 7c902ce0fce7ee386321a9c4bbe43d64ae69cf42 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Fri, 5 Jun 2020 15:53:05 +0200 Subject: [PATCH 09/15] Proposal update --- text/0112-server-runtime.md | 82 ++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 1c02e52b..9bca79a3 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -10,13 +10,7 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Summary -The LWC engine has been designed from the beginning to run in an environment with access to the DOM APIs. To accommodate server-side rendering (SSR) requirements, we need a way to decouple the engine from the DOM APIs. The existing `@lwc/engine` will be replaced by 3 new packages: - -- `@lwc/engine-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. -- `@lwc/engine-dom` exposes LWC APIs available on the browser. -- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. - -> > TODO: Add more details +> TODO: Add more details ## Goals @@ -216,7 +210,7 @@ This package contains core logic shared by the different runtime environment inc - `registerDecorators` - `sanitizeAttribute` -On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed +On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed - `getComponentInternalDef(Ctor: typeof LightningElement): ComponentDef`: Get the internal component definition for a given LightningElement constructor. - `createVM(elm: HostElement, Ctor, options: { mode: 'open' | 'closed', owner: VM | null, renderer: Renderer }): VM`: Create a new View-Model (VM) associated with an LWC component. @@ -225,34 +219,49 @@ On top of the public APIs, this package also exposes new internal APIs that are - `getAssociatedVMIfPresent(elm: HostElement): VM | undefined`: Retrieve the VM on a given element. - `setElementProto(elm: HostElement): void`: Patch an element prototype with the bridge element. -The DOM APIs used by the rendered engine are injected by the runtime depending on the environment. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). The [Renderer](#renderer-interface) interface will replace those direct invocation to the DOM API usages. +The current `@lwc/engine` code relies on direct DOM invocation. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). Those direct DOM APIs invocations will be replaced by a [`Rendered` interface](#renderer-interface) that will be injected at runtime into the `createVM`. All the sub-components created from the root VM will share the same `Renderer` interface. #### `@lwc/engine-dom` -Runtime that can be used to render LWC component trees in a DOM environment. This package exposes the following APIs: - -- `createElement` + reaction hooks -- `isNodeFromTemplate` +Runtime that can be used to render LWC component trees in a DOM environment. On top of re-exporting all the public APIs from the `@lwc/engine-core` package, this package also exposes the following DOM environment specific APIs: -This package injects the native DOM APIs into the `@lwc/engine-core` rendering engine. +- `createElement(name: string, ctor: typeof LightningElement): HTMLElement` +- `isNodeFromTemplate(node: Node): boolean` +- `getComponentConstructor(element: HTMLElement): typeof LightningElement | null` +- `buildCustomElementConstructor(ctor: typeof LightningElement): typeof HTMLElement` (deprecated) #### `@lwc/engine-server` -Runtime that can be used to render LWC component trees as strings. This package exposes the following API: +Runtime that can be used to render LWC component trees as strings. Like the `@lwc/engine-dom`, this package re-exports the all the public APIs from the `@lwc/engine-core` package along with: + +- `renderComponent(name: string, ctor: typeof LightningElement, props?: { [key: string]: any }): string`: This method creates an renders an LWC component synchronously to a string. It will be discussed further in the following section. + +### Rendering an LWC component on the server + +#### Constraints + +In order to make the LWC SSR predictible and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints. + +**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server side renderer component wants to attached some event listeners to the `document` when it is rendered on the client, it need to check first if the `document` object is present in the current runtime environment. + +**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by avoid creating unnecessary JavaScript `Proxy`. This also means that if a component need to do an asynchronous operation to fetch data to render, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior rendering the component and needs to be passed via public property for it to be rendered. + +**The `renderedCallback` lifecycle hook will not execute on the server:** When running a browser, this hook is the first life cycle hook which give the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would results in a runtime error since the DOM APIs are not polyfilled. -- `renderComponent(name: string, ctor: typeof LightningElement, props?: Record): string`: This method creates an renders an LWC component synchronously to a string. +**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define today a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any of the wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapter compatible with SSR. -When a component is rendered using `renderComponent`, the following restriction applies: +#### The `renderComponent` API -- each created component will execute the following life-cycle hooks once `constructor`, `connectedCallback` and `render` -- properties and methods annotated with `@wire` will not be invoked -- component reactivity is disabled +The `renderComponent` is the only new public API exposed with this proposal. This new API is only available in `@lwc/engine-server`. It renders a component and returns synchronously the rendered content. This proposal accepts 3 arguments: + - `name` (type: `string`) - the tag name of the component host element. + - `ctor` (type: `typeof LightningElement`) - the root LWC component constructor. + - `props` (optional, type: `{ [key string]: any}`) - an object representing the different properties set on the root component. -Another API might be created to accommodate wire adapter invocation and asynchronous data fetching on the server. +This method returns a the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. -This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent` and only implements a restricted subset of the event dispatching algorithm on the server (no bubbling, no event retargeting). +#### Component Authoring format -The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. +The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. Since the LWC engine exposes different API depending on the environment, the application owner will be in charge of creating a different entry point for each environment. **`c/app/app.js`:** @@ -282,29 +291,10 @@ const str = renderComponent('c-app', { is: App }); console.log(str); ``` -## Drawbacks - -- Requires a complete rewrite of the current `@lwc/engine` -- Might require substantial changes to the existing tools (jest preset, rollup plugins, etc.) to load the right engine depending on the environment. - -## Alternatives - -### Using a DOM implementation in JavaScript before evaluating the engine - -Prior the evaluation of the LWC engine, we would evaluate a DOM implementation write in JavaScript (like [jsdom](https://github.com/jsdom/jsdom) or [domino](https://github.com/fgnass/domino)). The compelling aspect of this approach is that it requires almost no change to the engine to work. - -There are multiple drawbacks with this approach: - -- Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance. -- Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server. - ## How we teach this -This proposal breaks up the existing `@lwc/engine` package into multiple packages. Because of this APIs that used to be imported from `lwc` might not be present anymore. With this proposal, the `createElement` API will be moving to `@lwc/engine-dom`. This change constitutes a breaking change by itself, so we need to be careful how this change will be released. The good news is that `createElement` is considered as an experimental API and should only used to create the application root and to test components. - -Changing the application root creation consists in a one line change. Therefor it should be pretty straightforward updated as long as it is well documented. Updating all the usages of `createElement` in test will probably require more time. For testing purposes, the `lwc` module will be mapped to the `@lwc/engine-dom` instead of `@lwc/engine-core` for now. We will also add warning messages in the console to promulgate the new pattern. A codemod for test files can also be used rename the `lwc` import in the test files to `@lwc/engine-dom`. - -Updating the documentation for the newly added server only APIs should be enough. +- Updating the documentation for the newly added server only APIs should be enough. +- Creating a set of a new linting rules prevent obvious cases where components can't be rendered on the server. ## Unresolved questions @@ -473,7 +463,7 @@ Finally, on top of all the APIs used by the engine to evaluate and run, componen - **SSR Usage:** 🔶 MIGHT BE REQUIRED - **Observations:** This might not be needed because `CustomEvent` inherits from `Event` and because `CustomEvent` is the recommended way to dispatch non-standard events. -### Renderer interface +### `Renderer` interface ```ts export interface Renderer { @@ -527,4 +517,4 @@ export interface Renderer { isConnected(node: HostNode): boolean; tagName(element: HostElement): string; } -``` +``` \ No newline at end of file From d3231ecc979b646445db899d0262053a28082c33 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Fri, 5 Jun 2020 16:09:45 +0200 Subject: [PATCH 10/15] Fix typos --- text/0112-server-runtime.md | 52 +++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/text/0112-server-runtime.md b/text/0112-server-runtime.md index 9bca79a3..9302aee2 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-runtime.md @@ -14,36 +14,38 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Goals -Server Side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static website, … +Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static websites, … Due to the size and complexity of this feature, the rollout of SSR is broken up in 3 different phases and is spread over multiple releases: -- **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). -- **Phase 2:** Settle on a HTML serialization format -- **Phase 3:** Rehydrate in a browser the DOM tree resulting of the serialization into an LWC component tree +- **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase, the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). +- **Phase 2:** Settle on an HTML serialization format. +- **Phase 3:** Rehydrate in a browser the DOM tree resulting from the serialization into an LWC component tree. -As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component stand point, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms. +As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component standpoint, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms. ## Proposal Scope -This proposal will be only focussing on the phase 1 of the SSR project. The phase 2 and 3 will be covered in subsequent proposals. This proposal cover 2 main aspects of the LWC SSR: +This proposal will be only focussing on phase 1 of the SSR project. Phase 2 and 3 will be covered in subsequent proposals. This proposal cover 2 main aspects of the LWC SSR: -- What are the differences between the DOM version and the SSR version of LWC? -- How to create 2 different implementation of the LWC engine while keeping the core logic shared. +- What are the different approach in implement SSR in a UI framework? +- How to create 2 different implementations of the LWC engine while keeping the core logic shared? +- What new APIs need to be exposed to render components on the server? +- What constraints need to be put in place to ensure that SSR is reliable and performant? ## Detailed design ### Approaching SSR in UI framework -Many popular UI framework already provide options to enable server side rendering. There is no single way to implement SSR in a UI framework and each framework approach this in a different way. That being said all the major frameworks converge around 2 main approaches. +Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR in a UI framework and each framework approaches this in a different way. That being said all the major frameworks converge around 2 main approaches. -The first approach is to **polyfill the server environment with DOM APIs and to reuse same core UI framework code on the server and on the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr). +The first approach is to **polyfill the server environment with DOM APIs and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr). -This approach is really convenient because it require almost no change to the core UI framework to enable SSR. Since most of the DOM APIs used by UI framework are low level dom API, most of them are easy to polyfill. Popular DOM APIs implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to polyfills. +This approach is convenient because it requires almost no change to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level API, most of them are easy to polyfill. Popular DOM APIs implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to polyfills. -This approach also suffers from multiple drawback. The main issue is that the polyfill has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers has to always double check that the newly introduced APIs are also polyfilled properly. The other main drawback, is that by applying polyfills to evaluate the engine the same DOM interfaces and methods a also from the component code (eg. `window`, `document`, ...) which gives a false senses that the component is running in the browser. +This approach also suffers from multiple drawbacks. The main issue is that the polyfill has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also polyfilled properly. The other main drawback is that applying polyfills to evaluate the engine the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. -Using the polyfilling approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC of would look something like this. +Using the polyfilling approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like this. **`index.ts`:** @@ -99,13 +101,13 @@ Object.assign(globalThis, { }); ``` -The second approach is to **abstract the DOM APIs and the core UI framework**. This approach is used by [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src). This approach consists in creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded server the rendering APIs are manipulating string to create the serialized version of the components on the fly. +The second approach is to **abstract the DOM APIs and the core UI framework**. This approach is used by [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src). This approach consists of creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded server the rendering APIs are manipulating string to create the serialized version of the components on the fly. -When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting at runtime this indirection, it also ensure that component code don't have access to any DOM APIs when running on the server. +When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting at runtime this indirection, it also ensures that component code doesn't have access to any DOM APIs when running on the server. -The main drawback with this approach is the amount of refactor that need to happen in LWC engine code specifically. So far the LWC engine has been designed to run in JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. +The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. -In order to inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. You can find below some pseudo code for each entry point. The 2 entry points uses the same `createComponent` abstraction to create an LWC component but pass a renderer specific to the targeted environment. +To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. You can find below some pseudo-code for each entry point. The 2 entry points use the same `createComponent` abstraction to create an LWC component but pass a renderer specific to the targeted environment. **`entry-points/dom.js`:** @@ -185,15 +187,15 @@ export function renderComponent( } ``` -**Conclusion:** After weighting the pros and cons of the 2 approaches described above, **we decided to go with the second approach when the rendering APIs are injected lazily at runtime**. +**Conclusion:** After considering the pros and cons of the 2 approaches described above, **we decided to go with the second approach when the rendering APIs are injected lazily at runtime**. ### Splitting `@lwc/engine` -As discussed in the previous section, an abstraction of the the underlying rendering APIs into the core framework depending on the target environment. In order to share the core UI framework between the different environment the existing `@lwc/engine` will be split into 3 packages. +As discussed in the previous section, an abstraction of the underlying rendering APIs into the core framework depending on the target environment. To share the core UI framework between the different environments the existing `@lwc/engine` will be split into 3 packages. #### `@lwc/engine-core` -This package contains core logic shared by the different runtime environment including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This packages exposes the following platform-agnostic public APIs: +This package contains core logic shared by the different runtimes environment including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This package exposes the following platform-agnostic public APIs: - `LightningElement` - `api` @@ -223,7 +225,7 @@ The current `@lwc/engine` code relies on direct DOM invocation. The list of all #### `@lwc/engine-dom` -Runtime that can be used to render LWC component trees in a DOM environment. On top of re-exporting all the public APIs from the `@lwc/engine-core` package, this package also exposes the following DOM environment specific APIs: +A runtime that can be used to render LWC component trees in a DOM environment. On top of re-exporting all the public APIs from the `@lwc/engine-core` package, this package also exposes the following DOM environment specific APIs: - `createElement(name: string, ctor: typeof LightningElement): HTMLElement` - `isNodeFromTemplate(node: Node): boolean` @@ -232,7 +234,7 @@ Runtime that can be used to render LWC component trees in a DOM environment. On #### `@lwc/engine-server` -Runtime that can be used to render LWC component trees as strings. Like the `@lwc/engine-dom`, this package re-exports the all the public APIs from the `@lwc/engine-core` package along with: +A runtime that can be used to render LWC component trees as strings. Like the `@lwc/engine-dom`, this package re-exports all the public APIs from the `@lwc/engine-core` package along with: - `renderComponent(name: string, ctor: typeof LightningElement, props?: { [key: string]: any }): string`: This method creates an renders an LWC component synchronously to a string. It will be discussed further in the following section. @@ -240,11 +242,11 @@ Runtime that can be used to render LWC component trees as strings. Like the `@lw #### Constraints -In order to make the LWC SSR predictible and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints. +To make the LWC SSR predictable and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints. -**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server side renderer component wants to attached some event listeners to the `document` when it is rendered on the client, it need to check first if the `document` object is present in the current runtime environment. +**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server-side renderer component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. -**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by avoid creating unnecessary JavaScript `Proxy`. This also means that if a component need to do an asynchronous operation to fetch data to render, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior rendering the component and needs to be passed via public property for it to be rendered. +**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by avoiding creating unnecessary JavaScript `Proxy`. This also means that if a component need to do an asynchronous operation to fetch data to render, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior rendering the component and needs to be passed via public property for it to be rendered. **The `renderedCallback` lifecycle hook will not execute on the server:** When running a browser, this hook is the first life cycle hook which give the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would results in a runtime error since the DOM APIs are not polyfilled. From d2cdaf77fa041dda0b1cd5e9e8ebace6bbc48222 Mon Sep 17 00:00:00 2001 From: Ted Date: Fri, 5 Jun 2020 17:41:58 +0200 Subject: [PATCH 11/15] fix: cleaning up a bit and adding discussed suggestions --- ...erver-runtime.md => 0112-server-engine.md} | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) rename text/{0112-server-runtime.md => 0112-server-engine.md} (74%) diff --git a/text/0112-server-runtime.md b/text/0112-server-engine.md similarity index 74% rename from text/0112-server-runtime.md rename to text/0112-server-engine.md index 9302aee2..432c2d10 100644 --- a/text/0112-server-runtime.md +++ b/text/0112-server-engine.md @@ -1,12 +1,12 @@ --- -title: LWC server runtime +title: LWC Server Engine status: DRAFTED created_at: 2020-01-06 updated_at: 2020-01-06 pr: https://github.com/salesforce/lwc-rfcs/pull/23 --- -# LWC server runtime +# LWC Server Engine ## Summary @@ -14,9 +14,9 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Goals -Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static websites, … +Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser, on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used for many different things: improving initial page render time, generating static websites, reading site content for SEO bots, indexing content for site search, etc. -Due to the size and complexity of this feature, the rollout of SSR is broken up in 3 different phases and is spread over multiple releases: +Due to the size and complexity of this feature, the rollout of SSR is broken up into 3 different phases and is spread over multiple releases: - **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase, the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). - **Phase 2:** Settle on an HTML serialization format. @@ -26,31 +26,31 @@ As part of phase 1, the goal is to provide a capability for LWC to render compon ## Proposal Scope -This proposal will be only focussing on phase 1 of the SSR project. Phase 2 and 3 will be covered in subsequent proposals. This proposal cover 2 main aspects of the LWC SSR: +This proposal will be only focusing on phase 1 of the SSR project. Phase 2 and 3 will be covered in subsequent proposals. This proposal covers the following aspects of LWC SSR: -- What are the different approach in implement SSR in a UI framework? -- How to create 2 different implementations of the LWC engine while keeping the core logic shared? +- What are the different possible approaches for implementing SSR in a UI framework? +- How to create two different implementations of the LWC engine while keeping the core logic shared? - What new APIs need to be exposed to render components on the server? - What constraints need to be put in place to ensure that SSR is reliable and performant? ## Detailed design -### Approaching SSR in UI framework +### How do other frameworks implement SSR? -Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR in a UI framework and each framework approaches this in a different way. That being said all the major frameworks converge around 2 main approaches. +Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR in a UI framework and each framework approaches this in a different way. That being said, all the major frameworks converge around 2 main approaches. -The first approach is to **polyfill the server environment with DOM APIs and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr). +The first approach is to **mock the DOM APIs in the server environment and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr) took. -This approach is convenient because it requires almost no change to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level API, most of them are easy to polyfill. Popular DOM APIs implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to polyfills. +This approach is convenient because it requires almost no changes to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level APIs, most of them are easy to mock. Popular DOM API implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to mocks. -This approach also suffers from multiple drawbacks. The main issue is that the polyfill has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also polyfilled properly. The other main drawback is that applying polyfills to evaluate the engine the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. +This approach also suffers from multiple drawbacks. The main issue is that the mock has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also properly mocked. The other main drawback is that by applying polyfills to evaluate the engine, the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. -Using the polyfilling approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like this. +Using the mocking approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like this. -**`index.ts`:** +**`server.ts`:** ```ts -// Evaluate fist the DOM polyfills, then the DOM engine version of the engine. Reexport all the +// Evaluate fist the DOM polyfills, then the DOM engine version of the engine. Re-export all the // exported properties from the DOM engine. import './dom-environment'; export * from '@lwc/engine'; @@ -101,13 +101,17 @@ Object.assign(globalThis, { }); ``` -The second approach is to **abstract the DOM APIs and the core UI framework**. This approach is used by [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src). This approach consists of creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded server the rendering APIs are manipulating string to create the serialized version of the components on the fly. +The second approach is to **abstract the DOM APIs and the core UI framework** which is what [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src) did. This consists of creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser, the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded on the server the rendering APIs are manipulating string to create the serialized version of the components on the fly. -When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting at runtime this indirection, it also ensures that component code doesn't have access to any DOM APIs when running on the server. +When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting this indirection at runtime, it also ensures that component code doesn't have access to any DOM APIs when running on the server. -The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. +> TODO: discuss the architectural drawback -To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. You can find below some pseudo-code for each entry point. The 2 entry points use the same `createComponent` abstraction to create an LWC component but pass a renderer specific to the targeted environment. +The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in a JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code stores a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. + +> TODO: describe the main differences between the two approaches + +To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass a different renderer specific to the target environment. **`entry-points/dom.js`:** @@ -187,15 +191,17 @@ export function renderComponent( } ``` -**Conclusion:** After considering the pros and cons of the 2 approaches described above, **we decided to go with the second approach when the rendering APIs are injected lazily at runtime**. +**Conclusion:** After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach, where the rendering APIs are injected lazily at runtime.** + +## How will we implement SSR? ### Splitting `@lwc/engine` -As discussed in the previous section, an abstraction of the underlying rendering APIs into the core framework depending on the target environment. To share the core UI framework between the different environments the existing `@lwc/engine` will be split into 3 packages. +As discussed in the previous section, we will introduce an abstraction of the underlying rendering APIs into the core framework depending on the target environment. To share the core UI framework between the different environments the existing `@lwc/engine` will be split into three packages. #### `@lwc/engine-core` -This package contains core logic shared by the different runtimes environment including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This package exposes the following platform-agnostic public APIs: +This package contains core logic shared by the different runtime environments including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This package exposes the following platform-agnostic public APIs: - `LightningElement` - `api` @@ -212,7 +218,7 @@ This package contains core logic shared by the different runtimes environment in - `registerDecorators` - `sanitizeAttribute` -On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed +On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed by the platform specific modules: - `getComponentInternalDef(Ctor: typeof LightningElement): ComponentDef`: Get the internal component definition for a given LightningElement constructor. - `createVM(elm: HostElement, Ctor, options: { mode: 'open' | 'closed', owner: VM | null, renderer: Renderer }): VM`: Create a new View-Model (VM) associated with an LWC component. @@ -221,7 +227,7 @@ On top of the public APIs, this package also exposes new internal APIs that are - `getAssociatedVMIfPresent(elm: HostElement): VM | undefined`: Retrieve the VM on a given element. - `setElementProto(elm: HostElement): void`: Patch an element prototype with the bridge element. -The current `@lwc/engine` code relies on direct DOM invocation. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). Those direct DOM APIs invocations will be replaced by a [`Rendered` interface](#renderer-interface) that will be injected at runtime into the `createVM`. All the sub-components created from the root VM will share the same `Renderer` interface. +The current `@lwc/engine` code relies on direct DOM invocation. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). Those direct DOM API invocations will be replaced by a [`Rendered` interface](#renderer-interface) that will be injected at runtime into the `createVM`. All the sub-components created from the root VM will share the same `Renderer` interface. #### `@lwc/engine-dom` @@ -234,36 +240,44 @@ A runtime that can be used to render LWC component trees in a DOM environment. O #### `@lwc/engine-server` -A runtime that can be used to render LWC component trees as strings. Like the `@lwc/engine-dom`, this package re-exports all the public APIs from the `@lwc/engine-core` package along with: +A runtime that can be used to render LWC component trees as strings. This package re-exports all the public APIs from the `@lwc/engine-core` package along with: -- `renderComponent(name: string, ctor: typeof LightningElement, props?: { [key: string]: any }): string`: This method creates an renders an LWC component synchronously to a string. It will be discussed further in the following section. +- `renderComponent(name: string, ctor: typeof LightningElement, props?: { [key: string]: any }): string`: This method creates and renders an LWC component synchronously to a string. It will be discussed further in the following section. ### Rendering an LWC component on the server #### Constraints -To make the LWC SSR predictable and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints. +To make the LWC SSR predictable and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints: + +**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server-side rendered component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. + +> TODO: code snippet example + +**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by not creating unnecessary JavaScript `Proxy`. + +> TODO: react does that -**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server-side renderer component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. +**No asynchronous operations allowed:** This also means that if a component needs to do an asynchronous operation to fetch data, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior to rendering the component and needs to be passed via public properties for it to be rendered. -**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by avoiding creating unnecessary JavaScript `Proxy`. This also means that if a component need to do an asynchronous operation to fetch data to render, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior rendering the component and needs to be passed via public property for it to be rendered. +> TODO: elaborate why -**The `renderedCallback` lifecycle hook will not execute on the server:** When running a browser, this hook is the first life cycle hook which give the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would results in a runtime error since the DOM APIs are not polyfilled. +**The `renderedCallback` lifecycle hook will not execute on the server:** When running in a browser, this hook is the first life cycle hook which gives the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would result in a runtime error since the DOM APIs are not mocked. -**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define today a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any of the wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapter compatible with SSR. +**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapter compatible with SSR. #### The `renderComponent` API -The `renderComponent` is the only new public API exposed with this proposal. This new API is only available in `@lwc/engine-server`. It renders a component and returns synchronously the rendered content. This proposal accepts 3 arguments: +The `renderComponent` is the only new public API exposed with this proposal. This new API is only available in `@lwc/engine-server`. It renders a component and synchronously returns the rendered content. This proposal accepts 3 arguments: - `name` (type: `string`) - the tag name of the component host element. - `ctor` (type: `typeof LightningElement`) - the root LWC component constructor. - `props` (optional, type: `{ [key string]: any}`) - an object representing the different properties set on the root component. -This method returns a the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. +This method returns a the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However, in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. #### Component Authoring format -The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. Since the LWC engine exposes different API depending on the environment, the application owner will be in charge of creating a different entry point for each environment. +The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. Since the LWC engine exposes different APIs depending on the environment, the application owner will be in charge of creating a different entry point for each environment. **`c/app/app.js`:** @@ -298,9 +312,9 @@ console.log(str); - Updating the documentation for the newly added server only APIs should be enough. - Creating a set of a new linting rules prevent obvious cases where components can't be rendered on the server. -## Unresolved questions +## Open questions -- **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring the type safety. +- **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring type safety. - **How to implement LWC Context for SSR?** Context uses eventing for registration between the provider and the consumer. Since `@lwc/engine-server` will only implement a subset of the DOM eventing, we will need to evaluate how we can replace the current registration mechanism. --- From 3bc08a5ebc5a01c53a0040d9a95443300c784136 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Sat, 6 Jun 2020 11:49:46 +0200 Subject: [PATCH 12/15] Address comments --- text/0112-server-engine.md | 52 +++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/text/0112-server-engine.md b/text/0112-server-engine.md index 432c2d10..556dd9d1 100644 --- a/text/0112-server-engine.md +++ b/text/0112-server-engine.md @@ -2,7 +2,8 @@ title: LWC Server Engine status: DRAFTED created_at: 2020-01-06 -updated_at: 2020-01-06 +updated_at: 2020-06-06 +champion: Pierre-Marie Dartus (@pmdartus) pr: https://github.com/salesforce/lwc-rfcs/pull/23 --- @@ -10,10 +11,6 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Summary -> TODO: Add more details - -## Goals - Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser, on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used for many different things: improving initial page render time, generating static websites, reading site content for SEO bots, indexing content for site search, etc. Due to the size and complexity of this feature, the rollout of SSR is broken up into 3 different phases and is spread over multiple releases: @@ -39,6 +36,8 @@ This proposal will be only focusing on phase 1 of the SSR project. Phase 2 and 3 Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR in a UI framework and each framework approaches this in a different way. That being said, all the major frameworks converge around 2 main approaches. +#### DOM APIs mocking + The first approach is to **mock the DOM APIs in the server environment and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr) took. This approach is convenient because it requires almost no changes to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level APIs, most of them are easy to mock. Popular DOM API implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to mocks. @@ -101,15 +100,13 @@ Object.assign(globalThis, { }); ``` +#### Runtime injection of the rendering APIs + The second approach is to **abstract the DOM APIs and the core UI framework** which is what [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src) did. This consists of creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser, the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded on the server the rendering APIs are manipulating string to create the serialized version of the components on the fly. When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting this indirection at runtime, it also ensures that component code doesn't have access to any DOM APIs when running on the server. -> TODO: discuss the architectural drawback - -The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in a JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code stores a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. - -> TODO: describe the main differences between the two approaches +From a drawback perspective, introducing this a layer of indirection between the core logic and underlying APIs might introduce a performance overhead and increase the overall size of the engine. To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass a different renderer specific to the target environment. @@ -191,7 +188,11 @@ export function renderComponent( } ``` -**Conclusion:** After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach, where the rendering APIs are injected lazily at runtime.** +#### Retained approach + +After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach, where the rendering APIs are injected lazily at runtime.** + +The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in a JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code stores a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. ## How will we implement SSR? @@ -252,15 +253,32 @@ To make the LWC SSR predictable and performant, only a certain subset of the LWC **No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server-side rendered component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. -> TODO: code snippet example +```js +import { LightningElement } from 'lwc'; + +// Will evaluate to true when running in a browser, otherwise it will evaluate to false. +const isBrowser = typeof document !== 'undefined'; + +export default class App extends LightningElement { + connectedCallback() { + if (isBrowser) { + document.addEventListener('click', this.handleDocumentClick); + } + } -**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by not creating unnecessary JavaScript `Proxy`. + disconnectedCallback() { + if (isBrowser) { + document.removeEventListener('click', this.handleDocumentClick); + } + } -> TODO: react does that + handleDocumentClick() { /* ... */ } +} +``` -**No asynchronous operations allowed:** This also means that if a component needs to do an asynchronous operation to fetch data, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior to rendering the component and needs to be passed via public properties for it to be rendered. +**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by not creating unnecessary JavaScript `Proxy`. -> TODO: elaborate why +**No asynchronous operations allowed:** This also means that if a component needs to do an asynchronous operation to fetch data, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior to rendering the component and needs to be passed via public properties for it to be rendered. Without adding new primitives to the LWC framework updating the state of a component asynchronously would violate the previous constraints. Other popular UI frameworks are currently working on supporting asynchronous rendering and integrating it their current SSR solution, but it is a complex feature to implement. **The `renderedCallback` lifecycle hook will not execute on the server:** When running in a browser, this hook is the first life cycle hook which gives the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would result in a runtime error since the DOM APIs are not mocked. @@ -275,7 +293,7 @@ The `renderComponent` is the only new public API exposed with this proposal. Thi This method returns a the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However, in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. -#### Component Authoring format +#### Component Authoring Format The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. Since the LWC engine exposes different APIs depending on the environment, the application owner will be in charge of creating a different entry point for each environment. From 3f5f857cd44539415b0907e24c0d1b361eba5d56 Mon Sep 17 00:00:00 2001 From: Eugene Kashida Date: Mon, 8 Jun 2020 00:25:08 -0700 Subject: [PATCH 13/15] chore: editing (1) --- text/0112-server-engine.md | 54 ++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/text/0112-server-engine.md b/text/0112-server-engine.md index 556dd9d1..559e6443 100644 --- a/text/0112-server-engine.md +++ b/text/0112-server-engine.md @@ -11,19 +11,19 @@ pr: https://github.com/salesforce/lwc-rfcs/pull/23 ## Summary -Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser, on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used for many different things: improving initial page render time, generating static websites, reading site content for SEO bots, indexing content for site search, etc. +Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser, on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or more stylesheets. This technique can be used for many different things: improving initial page render time, generating static websites, reading site content for SEO bots, indexing content for site search, etc. -Due to the size and complexity of this feature, the rollout of SSR is broken up into 3 different phases and is spread over multiple releases: +Due to the size and complexity of this feature, the rollout of SSR will be broken up into 3 different phases spread over multiple releases: - **Phase 1:** Decouple the LWC engine from the DOM and allow it to run in a JavaScript environment without access to the DOM APIs (like Node.js). As part of this phase, the LWC server engine will also generate a prototype version of the serialized HTML (Phase 2). -- **Phase 2:** Settle on an HTML serialization format. -- **Phase 3:** Rehydrate in a browser the DOM tree resulting from the serialization into an LWC component tree. +- **Phase 2:** Settle on an HTML serialization format and update the prototype version. +- **Phase 3:** Rehydrate the serialized DOM tree into an LWC component tree in the browser. -As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component standpoint, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms. +As part of phase 1, the goal is to provide the capability for LWC to render components transparently on the server. From the component standpoint, regardless of whether we need to render the component on the server or in the DOM, the APIs used to author a component should be identical. ## Proposal Scope -This proposal will be only focusing on phase 1 of the SSR project. Phase 2 and 3 will be covered in subsequent proposals. This proposal covers the following aspects of LWC SSR: +This proposal will only focus on phase 1 described above. Phase 2 and 3 will be covered in subsequent proposals. This proposal covers the following aspects of LWC SSR: - What are the different possible approaches for implementing SSR in a UI framework? - How to create two different implementations of the LWC engine while keeping the core logic shared? @@ -34,23 +34,23 @@ This proposal will be only focusing on phase 1 of the SSR project. Phase 2 and 3 ### How do other frameworks implement SSR? -Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR in a UI framework and each framework approaches this in a different way. That being said, all the major frameworks converge around 2 main approaches. +Many popular UI frameworks already provide options to enable server-side rendering. There is no single way to implement SSR and each framework approaches it in a different way. That being said, all the major frameworks converge around 2 main approaches. -#### DOM APIs mocking +#### Mocking the DOM APIs -The first approach is to **mock the DOM APIs in the server environment and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom) and [lit-ssr](https://github.com/PolymerLabs/lit-ssr) took. +The first approach is to **mock the DOM APIs in the server environment and to reuse the same core UI framework code on the server and the client**. This is the approach [Stencil](https://github.com/ionic-team/stencil/master/tree/src/mock-doc), [Ember](https://github.com/ember-fastboot/simple-dom), and [lit-ssr](https://github.com/PolymerLabs/lit-ssr) take. This approach is convenient because it requires almost no changes to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level APIs, most of them are easy to mock. Popular DOM API implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to mocks. -This approach also suffers from multiple drawbacks. The main issue is that the mock has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also properly mocked. The other main drawback is that by applying polyfills to evaluate the engine, the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. +This approach also suffers from multiple drawbacks. The main issue is that the mock has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also properly mocked. Another issue is that by applying polyfills to evaluate the engine, the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. -Using the mocking approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like this. +Using the mocking approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like the following. **`server.ts`:** ```ts -// Evaluate fist the DOM polyfills, then the DOM engine version of the engine. Re-export all the -// exported properties from the DOM engine. +// Evaluate the DOM polyfills before the DOM engine. Re-export all the exported properties from the +// DOM engine. import './dom-environment'; export * from '@lwc/engine'; ``` @@ -102,11 +102,11 @@ Object.assign(globalThis, { #### Runtime injection of the rendering APIs -The second approach is to **abstract the DOM APIs and the core UI framework** which is what [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom) and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src) did. This consists of creating an indirection between the rendering APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser, the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. While when loaded on the server the rendering APIs are manipulating string to create the serialized version of the components on the fly. +The second approach is to **abstract the DOM APIs and the core UI framework**. This is what [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom), and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src) do. This involves an indirection between the DOM APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser, the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. When loaded on the server, the rendering APIs manipulate strings to serialize components on the fly. -When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all the environments. By injecting this indirection at runtime, it also ensures that component code doesn't have access to any DOM APIs when running on the server. +When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all environments. By injecting this indirection at runtime, it also ensures that component code doesn't have access to actual DOM APIs when running on the server. -From a drawback perspective, introducing this a layer of indirection between the core logic and underlying APIs might introduce a performance overhead and increase the overall size of the engine. +From a drawback perspective, introducing this layer of indirection between the core logic and underlying APIs might introduce a performance overhead and increase the overall size of the engine. To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass a different renderer specific to the target environment. @@ -119,7 +119,11 @@ const domRenderer = { createElement(tagName: string): Element { return document.createElement(tagName); }, - insertBefore(node: Node, parent: Element, anchor: Node): void { + insertBefore( + node: Node, + parent: Element, + anchor: Node + ): void { return parent.insertBefore(node, anchor); }, }; @@ -172,7 +176,7 @@ const serverRenderer = { }; // The method dedicated to create an LWC component in a server environment. It returns a string -// resulting of the serialization component tree +// representing the serialized component tree. export function renderComponent( tagName: string, opts: { Ctor: typeof LightningElement }, @@ -190,19 +194,19 @@ export function renderComponent( #### Retained approach -After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach, where the rendering APIs are injected lazily at runtime.** +After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach where the rendering APIs are injected lazily at runtime.** -The main drawback of this approach is the amount of refactoring that needs to happen in LWC engine code specifically. So far the LWC engine has been designed to run in a JavaScript environment with direct access to the DOM APIs. This means that if we were to adopt this approach a large chunk of the LWC engine code will need to be rewritten. For example some of the LWC engine code stores a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`) at evaluation time. This will not be possible with this approach as the APIs will be injected into the core engine after its evaluation. +The main drawback of this approach is the amount of refactoring that needs to happen in the LWC engine code. The present LWC engine has been designed to run in a JavaScript environment with direct access to DOM APIs. This means that if we were to adopt this approach, much of the LWC engine code will need to be rewritten. For example, parts of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`, etc) at evaluation time. This will not be possible in the injection approach as the APIs will not be available until after evaluation. ## How will we implement SSR? ### Splitting `@lwc/engine` -As discussed in the previous section, we will introduce an abstraction of the underlying rendering APIs into the core framework depending on the target environment. To share the core UI framework between the different environments the existing `@lwc/engine` will be split into three packages. +As discussed in the previous section, we will introduce an abstraction of the underlying rendering APIs into the core framework depending on the target environment. To share the core UI framework between the different environments, the existing `@lwc/engine` will be split into the following three packages. #### `@lwc/engine-core` -This package contains core logic shared by the different runtime environments including the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This package exposes the following platform-agnostic public APIs: +This package contains core logic shared by the different runtime environments. This includes the rendering engine and the reactivity mechanism. It should never be consumed directly in an application. It only provides internal APIs for building custom runtimes. This package exposes the following platform-agnostic public APIs: - `LightningElement` - `api` @@ -219,7 +223,7 @@ This package contains core logic shared by the different runtime environments in - `registerDecorators` - `sanitizeAttribute` -On top of the public APIs, this package also exposes new internal APIs that are meant to be consumed by the platform specific modules: +In addition, the following new APIs are also exposed. These are meant for internal use and are to be consumed by platform-specific modules: - `getComponentInternalDef(Ctor: typeof LightningElement): ComponentDef`: Get the internal component definition for a given LightningElement constructor. - `createVM(elm: HostElement, Ctor, options: { mode: 'open' | 'closed', owner: VM | null, renderer: Renderer }): VM`: Create a new View-Model (VM) associated with an LWC component. @@ -228,7 +232,7 @@ On top of the public APIs, this package also exposes new internal APIs that are - `getAssociatedVMIfPresent(elm: HostElement): VM | undefined`: Retrieve the VM on a given element. - `setElementProto(elm: HostElement): void`: Patch an element prototype with the bridge element. -The current `@lwc/engine` code relies on direct DOM invocation. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). Those direct DOM API invocations will be replaced by a [`Rendered` interface](#renderer-interface) that will be injected at runtime into the `createVM`. All the sub-components created from the root VM will share the same `Renderer` interface. +The current `@lwc/engine` code relies on direct DOM invocation. The list of all the current DOM APIs the engine depends upon can be found in the [DOM APIs usage](#dom-apis-usage). Those direct DOM API invocations will be replaced by a [`Renderer` interface](#renderer-interface) that will be injected at runtime via `createVM`. Sub-components created from the root VM will share the same `Renderer` interface. #### `@lwc/engine-dom` @@ -551,4 +555,4 @@ export interface Renderer { isConnected(node: HostNode): boolean; tagName(element: HostElement): string; } -``` \ No newline at end of file +``` From 7922372a0ee001166b0eb5279b80441dcf538390 Mon Sep 17 00:00:00 2001 From: Eugene Kashida Date: Mon, 8 Jun 2020 10:52:59 -0700 Subject: [PATCH 14/15] chore: editing (2) --- text/0112-server-engine.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/text/0112-server-engine.md b/text/0112-server-engine.md index 559e6443..44f18dd2 100644 --- a/text/0112-server-engine.md +++ b/text/0112-server-engine.md @@ -42,7 +42,7 @@ The first approach is to **mock the DOM APIs in the server environment and to re This approach is convenient because it requires almost no changes to the core UI framework to enable SSR. Since most of the DOM APIs used by the UI framework are low-level APIs, most of them are easy to mock. Popular DOM API implementations like [domino](https://github.com/fgnass/domino) or [jsdom](https://github.com/jsdom/jsdom) can be used to avoid having to write to mocks. -This approach also suffers from multiple drawbacks. The main issue is that the mock has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers have to always double-check that the newly introduced APIs are also properly mocked. Another issue is that by applying polyfills to evaluate the engine, the same DOM interfaces and methods are also available from the component code (eg. `window`, `document`, ...). This gives a false sense that the component is running in the browser. +This approach also suffers from multiple drawbacks. The main issue is that the mock has to stay in sync with the core UI framework. As the core UI framework evolves and the DOM APIs used by the engine changes, developers will always need to double-check that newly-introduced APIs are properly mocked. Another issue is that by applying polyfills to evaluate the engine, the same DOM interfaces and methods (eg. `window`, `document`, ...) will be available to components. This gives a false sense that the component is running in the browser. Using the mocking approach, the existing `@lwc/engine` stays untouched and the entry point of the SSR version of LWC would look something like the following. @@ -104,11 +104,11 @@ Object.assign(globalThis, { The second approach is to **abstract the DOM APIs and the core UI framework**. This is what [React](https://github.com/facebook/react/tree/master/packages/react-dom), [Vue](https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom), and [Angular](https://github.com/angular/angular/tree/master/packages/platform-browser/src) do. This involves an indirection between the DOM APIs and the core framework code. The indirection is injected at runtime depending on the environment. When loaded in the browser, the rendering APIs create DOM `Element` and set DOM `Attribute` on the elements. When loaded on the server, the rendering APIs manipulate strings to serialize components on the fly. -When used with a type-safe language like TypesScript, it is possible to ensure at compile time that all the rendering APIs are fulfilled in all environments. By injecting this indirection at runtime, it also ensures that component code doesn't have access to actual DOM APIs when running on the server. +When using a type-safe language like TypeScript, it is possible to ensure that all the rendering APIs are fulfilled in all environments at compile-time. In addition, injecting this indirection at runtime ensures that component code doesn't have access to actual DOM APIs when running on the server. From a drawback perspective, introducing this layer of indirection between the core logic and underlying APIs might introduce a performance overhead and increase the overall size of the engine. -To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass a different renderer specific to the target environment. +To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass different renderers specific to the target environment. **`entry-points/dom.js`:** @@ -196,7 +196,7 @@ export function renderComponent( After considering the pros and cons of the two approaches described above, **the rest of this proposal discusses the second approach where the rendering APIs are injected lazily at runtime.** -The main drawback of this approach is the amount of refactoring that needs to happen in the LWC engine code. The present LWC engine has been designed to run in a JavaScript environment with direct access to DOM APIs. This means that if we were to adopt this approach, much of the LWC engine code will need to be rewritten. For example, parts of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`, etc) at evaluation time. This will not be possible in the injection approach as the APIs will not be available until after evaluation. +The main drawback of this approach is the amount of refactoring that needs to happen in the LWC engine code. The present LWC engine has been designed to run in a JavaScript environment with direct access to DOM APIs. This means that if we were to adopt this approach, much of the LWC engine code will need to be refactored. For example, parts of the LWC engine code store a reference for DOM interfaces (eg. `HTMLElement.prototype`, `Node.prototype`, etc) at evaluation time. This will not be possible in the injection approach as the APIs will not be available until after evaluation. ## How will we implement SSR? @@ -255,7 +255,7 @@ A runtime that can be used to render LWC component trees as strings. This packag To make the LWC SSR predictable and performant, only a certain subset of the LWC engine capabilities available on the client will be present on the server. As a side-effect, LWC components that need to be rendered on the server will have to observe the following constraints: -**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server, will result in a runtime exception. For example, if a server-side rendered component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. +**No access to web platform APIs on the server:** When running in the server environment, LWC will not polyfill web platform-specific APIs. Because of this, accessing any of those APIs as the component renders on the server will result in a runtime exception. For example, if a server-rendered component wants to attach event listeners to the `document` when it is rendered on the client, it needs to check first if the `document` object is present in the current runtime environment. ```js import { LightningElement } from 'lwc'; @@ -280,24 +280,24 @@ export default class App extends LightningElement { } ``` -**The entire component tree will be rendered in a single pass synchronously:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by not creating unnecessary JavaScript `Proxy`. +**The entire component tree will be synchronously rendered in a single pass:** This behavior matches the current behavior of the LWC engine. Connecting an LWC component to a document triggers a synchronous rendering cycle. This means that reactivity is unnecessary on the server. Disabling the reactive membrane on the server will also improve the overall SSR performance by not creating unnecessary JavaScript `Proxies`. -**No asynchronous operations allowed:** This also means that if a component needs to do an asynchronous operation to fetch data, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior to rendering the component and needs to be passed via public properties for it to be rendered. Without adding new primitives to the LWC framework updating the state of a component asynchronously would violate the previous constraints. Other popular UI frameworks are currently working on supporting asynchronous rendering and integrating it their current SSR solution, but it is a complex feature to implement. +**No asynchronous operations allowed:** This also means that if a component needs to do an asynchronous operation to fetch data, it will not be possible to do so when rendered on the server. All the asynchronous data dependencies for a component subtree needs to be retrieved prior to rendering the component and needs to be passed via public properties for it to be rendered. Without adding new primitives to the LWC framework, updating the state of a component asynchronously would violate the previous constraints. Other popular UI frameworks are currently working on supporting asynchronous rendering and integrating it into their current SSR solution, but it is a complex feature to implement. **The `renderedCallback` lifecycle hook will not execute on the server:** When running in a browser, this hook is the first life cycle hook which gives the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would result in a runtime error since the DOM APIs are not mocked. -**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapter compatible with SSR. +**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapters compatible with SSR. #### The `renderComponent` API The `renderComponent` is the only new public API exposed with this proposal. This new API is only available in `@lwc/engine-server`. It renders a component and synchronously returns the rendered content. This proposal accepts 3 arguments: - - `name` (type: `string`) - the tag name of the component host element. - - `ctor` (type: `typeof LightningElement`) - the root LWC component constructor. - - `props` (optional, type: `{ [key string]: any}`) - an object representing the different properties set on the root component. + - `name` (type: `string`) - The tag name of the component host element. + - `ctor` (type: `typeof LightningElement`) - The root LWC component constructor. + - `props` (optional, type: `{ [key string]: any}`) - An object representing the different properties set on the root component. -This method returns a the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However, in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. +This method returns the serialized HTML content rendered by the component. The serialization format is out of the scope of this proposal and will be covered in a different RFC. However, in the first version of the `@lwc/engine-server`, the `renderComponent` will produce an HTML string matching the [declarative shadow DOM proposal](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) format. -#### Component Authoring Format +#### Component authoring format The existing `lwc` package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience writing a component remains identical. Since the LWC engine exposes different APIs depending on the environment, the application owner will be in charge of creating a different entry point for each environment. @@ -331,13 +331,13 @@ console.log(str); ## How we teach this -- Updating the documentation for the newly added server only APIs should be enough. -- Creating a set of a new linting rules prevent obvious cases where components can't be rendered on the server. +- Updating the documentation for the newly-added server-only APIs should be enough. +- Creating a set of new linting rules to prevent obvious cases where components can't be rendered on the server. ## Open questions -- **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs we will need to remove the `dom` lib from the `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring type safety. -- **How to implement LWC Context for SSR?** Context uses eventing for registration between the provider and the consumer. Since `@lwc/engine-server` will only implement a subset of the DOM eventing, we will need to evaluate how we can replace the current registration mechanism. +- **Remove `@lwc/engine-core` on TypeScript `dom` library?** The `@lwc/engine-core` package relies heavily on the ambient DOM TypeScript interfaces provided by the `dom` library. To ensure that the `@lwc/engine-core` is not leveraging any of the DOM APIs, we will need to remove the `dom` lib from `tsconfig.json`. It is currently unclear how all the ambient types can be removed on this package while ensuring type safety. +- **How to implement LWC Context for SSR?** Context relies on eventing for registration between providers and consumers. Since `@lwc/engine-server` will only implement a subset of DOM eventing, we will need to evaluate how we can replace the current registration mechanism. --- From d2fddd5f01560f25bbb6defaffa70e47ae9b297e Mon Sep 17 00:00:00 2001 From: Eugene Kashida Date: Wed, 10 Jun 2020 06:33:46 +0900 Subject: [PATCH 15/15] Apply suggestions from code review Thanks @jasonsilberman! Co-authored-by: Jason Silberman --- text/0112-server-engine.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0112-server-engine.md b/text/0112-server-engine.md index 44f18dd2..07d3dc62 100644 --- a/text/0112-server-engine.md +++ b/text/0112-server-engine.md @@ -110,7 +110,7 @@ From a drawback perspective, introducing this layer of indirection between the c To inject the rendering APIs depending on the environment, we would need to create a different entry point per environment. Below you can find some pseudo-code for each entry point. The two entry points use the same `createComponent` abstraction to create an LWC component but pass different renderers specific to the target environment. -**`entry-points/dom.js`:** +**`entry-points/dom.ts`:** ```ts import { LightningElement, createComponent } from '@lwc/engine-core'; @@ -140,7 +140,7 @@ export function createElement( } ``` -**`entry-points/server.js`:** +**`entry-points/server.ts`:** ```ts import { LightningElement, createComponent } from '@lwc/engine-core'; @@ -322,7 +322,7 @@ document.body.appendChild(app); **`server.js`:** ```js -import { renderComponent } from '@lwc/engine-server'; // Resolves to `@lwc/engine-server` +import { renderComponent } from 'lwc'; // Resolves to `@lwc/engine-server` import App from 'c/app'; const str = renderComponent('c-app', { is: App });