@lwrjs/lwc-ssr 0.7.0-alpha.9 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Server-side rendering (SSR) in LWR
2
+
3
+ ## Overview
4
+
5
+ ### What is SSR?
6
+
7
+ [Lightning Web Components (LWC)](https://lwc.dev/) is a framework for creating client-side applications. However, these components can also be rendered as an HTML string on the **server**. LWR sends these strings to the client, where they can be ["hydrated"](#client-hydration) to create an interactive web app.
8
+
9
+ ### Why use SSR?
10
+
11
+ With SSR, the browser does not need to wait for all the JavaScript to download and execute before displaying component markup. This results in faster time-to-content, especially on slower devices or internet connections. It also makes the content accessible to search engine crawlers, improving SEO.
12
+
13
+ That said, SSR is best used for apps where time-to-content is important, such as B2C websites. The benefits of SSR should be weighed against the costs: higher server load, increased build & deployment complexity, developing server-compatible component code.
14
+
15
+ ## Using SSR with LWR
16
+
17
+ Learn how to use SSR in your LWR apps.
18
+
19
+ ### Turn on SSR
20
+
21
+ SSR is activated on a per-route basis by changing `bootstrap.experimentalSSR` to `true`:
22
+
23
+ ```json
24
+ // my-app/lwr.config.json
25
+ {
26
+ "routes": [
27
+ {
28
+ "id": "ssr-page",
29
+ // parameterized path
30
+ "path": "/category/:category",
31
+ // content template
32
+ "contentTemplate": "$contentDir/page.html",
33
+ "bootstrap": {
34
+ // turn on SSR for the page here
35
+ "experimentalSSR": true
36
+ }
37
+ }
38
+ ]
39
+ }
40
+ ```
41
+
42
+ ### Building SSR pages
43
+
44
+ When a route with `experimentalSSR` is requested, LWR will use [LWC'S `renderComponent()` function](https://rfcs.lwc.dev/rfcs/lwc/0112-server-engine) to SSR each root component on the page. This is done whether the page is generated at runtime, or pre-built using `generateStaticSite()`.
45
+
46
+ > A "root component" is any lwc in an app route's [content template, layout template](https://github.com/salesforce/lwr-recipes/tree/main/packages/templating#templates), or [`rootComponent` configuration](https://github.com/salesforce/lwr-recipes/blob/main/doc/config.md#routes).
47
+
48
+ LWR will automatically pass any root component attributes from a [template](https://github.com/salesforce/lwr-recipes/tree/main/packages/templating#templates) as [public properties](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public) during SSR. For example, `my/root` will receive `{ limit: '10' }`.
49
+
50
+ ```html
51
+ <!-- my-app/src/content/page.html -->
52
+ <section>
53
+ <!-- "limit" is a template attribute property -->
54
+ <my-root limit="10"></my-root>
55
+ </section>
56
+ ```
57
+
58
+ #### Limitations
59
+
60
+ There are restrictions on component code for it to successfully render on the server. The `renderComponent()` function executes the [`constructor` and `connectedCallback`](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reference_lifecycle_hooks) of each component. These functions must be free of browser-specific code, such as DOM manipulation, eventing, and fetching data.
61
+
62
+ Because of this, the [`@lwrjs/router`](https://github.com/salesforce/lwr-recipes/blob/main/doc/navigation.md) is not supported with SSR.
63
+
64
+ ### Preloading data during SSR
65
+
66
+ Many components depend on external data. LWR provides a `getProps()` hook for developers to fetch that data on the server. LWR passes the data to the component during SSR as [properties](<(https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public)>).
67
+
68
+ > **Important**: This hook is **only** run for root components.
69
+
70
+ The `getProps()` hook is exported as a function from a root component module:
71
+
72
+ ```ts
73
+ // my-app/src/modules/my/root/root.ts
74
+ import { LightningElement, api } from 'lwc';
75
+ import type { PropsRequestContext, PropsResponse } from '@lwrjs/types';
76
+
77
+ export default class MyRoot extends LightningElement {
78
+ @api data: SomeDataType[] = [];
79
+ }
80
+
81
+ export async function getProps(context: PropsRequestContext): Promise<PropsResponse> {
82
+ // "/category/books" => context.params = { category: 'books' }
83
+ const category = context.params.category;
84
+ // page.html template => context.props = { limit: '10' }
85
+ const num = context.props.limit || '25';
86
+ const res = await context.fetch(`https://www.some-api.com/${category}?lang=${context.locale}&num=${num}`);
87
+ const data = await res.json();
88
+ return {
89
+ props: {
90
+ // will be passed to the root component as props
91
+ data,
92
+ ...context.props, // pass the template props through, if desired
93
+ },
94
+ };
95
+ }
96
+ ```
97
+
98
+ ```ts
99
+ type GetPropsHook = (context: PropsRequestContext) => Promise<PropsResponse>;
100
+
101
+ interface PropsRequestContext {
102
+ // existing props from template attributes
103
+ props: Json;
104
+ // values from a parameterized route defined in lwr.config.json
105
+ params: { [key: string]: string };
106
+ // search parameters from the request URL
107
+ query: { [key: string]: string };
108
+ // locale string for the request, eg: 'en-US'
109
+ locale: string;
110
+ // server-friendly fetch() function for accessing data
111
+ fetch: Function;
112
+ }
113
+
114
+ interface PropsResponse {
115
+ // serializable props for the root component
116
+ props: Json;
117
+ }
118
+
119
+ type Json = undefined | null | boolean | number | string | Json[] | { [prop: string]: Json };
120
+ ```
121
+
122
+ Notes:
123
+
124
+ - The `getProps()` hook can choose to merge the properties from `PropsRequestContext.props` into its return object, or it can ignore/discard them.
125
+ - The author of `getProps()` is responsible for validating the `params` and `query` from `PropsRequestContext` before using them.
126
+ - The **same** `props` returned by `getProps()` are passed to the component during server rendering **and** client hydration.
127
+
128
+ ### Client hydration
129
+
130
+ When SSRed component HTML reaches the browser, each root component is automatically hydrated. LWR uses the [LWC `hydrateComponent()` API](https://rfcs.lwc.dev/rfcs/lwc/0117-ssr-rehydration) to do so. Hydrating a component starts its component lifecycle and makes it interactive.
@@ -8,6 +8,14 @@ var __export = (target, all) => {
8
8
  // packages/@lwrjs/lwc-ssr/src/identity.ts
9
9
  __markAsModule(exports);
10
10
  __export(exports, {
11
- LWC_SSR_PREFIX: () => LWC_SSR_PREFIX
11
+ LWC_SSR_PREFIX: () => LWC_SSR_PREFIX,
12
+ SSR_PROPS_ATTR: () => SSR_PROPS_ATTR,
13
+ SSR_PROPS_KEY: () => SSR_PROPS_KEY,
14
+ getPropsId: () => getPropsId
12
15
  });
13
16
  var LWC_SSR_PREFIX = "@lwrjs/lwc-ssr/";
17
+ var SSR_PROPS_ATTR = "data-lwr-props-id";
18
+ var SSR_PROPS_KEY = "ssrProps";
19
+ function getPropsId() {
20
+ return `lwcprops${Math.floor(Math.random() * 65536).toString(16)}`;
21
+ }
@@ -29,18 +29,30 @@ __export(exports, {
29
29
  });
30
30
  var import_shared_utils = __toModule(require("@lwrjs/shared-utils"));
31
31
  var import_identity = __toModule(require("../identity.cjs"));
32
- function createSsrBootstrapModule(rootComponent) {
33
- return [
34
- "/* This module is generated and meant to be used in a Server context */",
35
- 'import { renderComponent } from "@lwc/engine-server"',
36
- `import Ctor from "${rootComponent}"`,
37
- "try {",
38
- ` const result = renderComponent("${(0, import_shared_utils.moduleSpecifierToKebabCase)(rootComponent)}", Ctor, globalThis.ssrProps);`,
39
- " globalThis.postback({ result });",
40
- "} catch(err) {",
41
- " globalThis.postback({ error: err.toString() });",
42
- `}`
43
- ].join("\n");
32
+ function createSsrBootstrapModule(rootSpecifier) {
33
+ return `
34
+ import { renderComponent } from '@lwc/engine-server';
35
+ import Ctor, * as rootComponent from '${rootSpecifier}';
36
+
37
+ (async () => {
38
+ try {
39
+ let props = globalThis.context.props;
40
+ if (rootComponent.getProps) {
41
+ const data = await rootComponent.getProps({
42
+ props,
43
+ params: globalThis.context.params,
44
+ query: globalThis.context.query,
45
+ locale: globalThis.context.locale,
46
+ fetch: globalThis.fetch,
47
+ });
48
+ props = data.props; // overwrite public props
49
+ }
50
+ const result = renderComponent('${(0, import_shared_utils.moduleSpecifierToKebabCase)(rootSpecifier)}', Ctor, props || {});
51
+ globalThis.postMessage({ result, props });
52
+ } catch(e) {
53
+ globalThis.postMessage({ error: e });
54
+ }
55
+ })()`;
44
56
  }
45
57
  var LwcSsrModuleProvider = class {
46
58
  constructor(providerConfig, {runtimeEnvironment: {lwrVersion}}) {
@@ -38,7 +38,7 @@ function lwcSsrViewTranformer(options, {moduleBundler}) {
38
38
  }
39
39
  if (viewContext.view.bootstrap?.experimentalSSR) {
40
40
  const ssrModules = [];
41
- for (const {tagName, location, props = {}} of customElements) {
41
+ for (const {tagName, location, props} of customElements) {
42
42
  if (location) {
43
43
  const {startOffset, endOffset} = location;
44
44
  const moduleSpecifier = (0, import_shared_utils.kebabCaseToModuleSpecifer)(tagName);
@@ -46,15 +46,26 @@ function lwcSsrViewTranformer(options, {moduleBundler}) {
46
46
  startOffset,
47
47
  endOffset,
48
48
  props,
49
+ tagName,
49
50
  specifier: `${import_identity.LWC_SSR_PREFIX}${moduleSpecifier}`
50
51
  });
51
52
  }
52
53
  }
53
- return Promise.all(ssrModules.map(({specifier, props, startOffset, endOffset}) => {
54
- return (0, import_ssr_element.ssrElement)({specifier, props}, moduleBundler, viewContext.runtimeEnvironment).then((ssrResult) => {
55
- stringBuilder.overwrite(startOffset, endOffset, ssrResult);
54
+ const ssrProps = {};
55
+ await Promise.all(ssrModules.map(({specifier, tagName, props, startOffset, endOffset}) => {
56
+ return (0, import_ssr_element.ssrElement)({specifier, props}, moduleBundler, viewContext).then(({html, props: props2}) => {
57
+ if (props2) {
58
+ const propsId = (0, import_identity.getPropsId)();
59
+ ssrProps[propsId] = props2;
60
+ const [, remain] = html.split(`<${tagName}`);
61
+ html = [`<${tagName}`, ` ${import_identity.SSR_PROPS_ATTR}="${propsId}"`, remain].join("");
62
+ }
63
+ stringBuilder.overwrite(startOffset, endOffset, html);
56
64
  });
57
- })).then(() => void 0);
65
+ }));
66
+ if (Object.keys(ssrProps).length) {
67
+ stringBuilder.prependLeft(ssrModules[0].startOffset, `<script type="application/javascript">globalThis.LWR = globalThis.LWR || {};globalThis.LWR.${import_identity.SSR_PROPS_KEY} = ${JSON.stringify(ssrProps)};</script>`);
68
+ }
58
69
  }
59
70
  }
60
71
  };
@@ -0,0 +1,52 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {get: all[name], enumerable: true});
11
+ };
12
+ var __exportStar = (target, module2, desc) => {
13
+ if (module2 && typeof module2 === "object" || typeof module2 === "function") {
14
+ for (let key of __getOwnPropNames(module2))
15
+ if (!__hasOwnProp.call(target, key) && key !== "default")
16
+ __defProp(target, key, {get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable});
17
+ }
18
+ return target;
19
+ };
20
+ var __toModule = (module2) => {
21
+ return __exportStar(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? {get: () => module2.default, enumerable: true} : {value: module2, enumerable: true})), module2);
22
+ };
23
+
24
+ // packages/@lwrjs/lwc-ssr/src/viewTransformer/sandbox.ts
25
+ __markAsModule(exports);
26
+ __export(exports, {
27
+ default: () => runCode
28
+ });
29
+ var import_worker_threads = __toModule(require("worker_threads"));
30
+ var HEADER = "/* This module is generated and meant to be used in a Server context */";
31
+ var WORKER_CODE_SANDBOX_APIS = [
32
+ `const { parentPort, workerData } = require('worker_threads');`,
33
+ `globalThis.context = workerData;`,
34
+ `globalThis.fetch = require('node-fetch');`,
35
+ `globalThis.postMessage = (...args) => parentPort.postMessage(...args);`
36
+ ];
37
+ function runCodeOnWorker(codes, workerData) {
38
+ const workerCode = [HEADER, ...WORKER_CODE_SANDBOX_APIS, ...codes].join("\n");
39
+ return new Promise((resolve) => {
40
+ const worker = new import_worker_threads.Worker(workerCode, {eval: true, workerData});
41
+ worker.on("message", resolve);
42
+ worker.on("error", ({message}) => resolve({error: `SSR worker exited with error: ${message}`}));
43
+ worker.on("exit", (code) => {
44
+ if (code !== 0) {
45
+ resolve({error: `SSR worker stopped with exit code: ${code}`});
46
+ }
47
+ });
48
+ });
49
+ }
50
+ function runCode(codes, context) {
51
+ return runCodeOnWorker(codes, context);
52
+ }
@@ -26,45 +26,34 @@ __markAsModule(exports);
26
26
  __export(exports, {
27
27
  ssrElement: () => ssrElement
28
28
  });
29
- var import_worker_threads = __toModule(require("worker_threads"));
30
29
  var import_amd_utils = __toModule(require("./amd-utils.cjs"));
30
+ var import_sandbox = __toModule(require("./sandbox.cjs"));
31
31
  var bundleConfigOverrides = {
32
32
  exclude: [],
33
33
  alias: {
34
34
  lwc: "@lwc/engine-server"
35
35
  }
36
36
  };
37
- var WORKER_CONTEXT_PARENTPORT = `const { parentPort } = require('worker_threads');`;
38
- var WORKER_CONTEXT_POSTBACK = `globalThis.postback = (result) => parentPort.postMessage(result);`;
39
- function runCodeOnWorker(codes) {
40
- const workerCode = [WORKER_CONTEXT_PARENTPORT, WORKER_CONTEXT_POSTBACK, ...codes].join("\n");
41
- return new Promise((resolve) => {
42
- const worker = new import_worker_threads.Worker(workerCode, {eval: true});
43
- worker.on("message", resolve);
44
- worker.on("error", ({message}) => resolve({error: `SSR worker exited with error: ${message}`}));
45
- worker.on("exit", (code) => {
46
- if (code !== 0) {
47
- resolve({error: `SSR worker stopped with exit code: ${code}`});
48
- }
49
- });
50
- });
51
- }
52
- async function ssrElement({specifier, props}, moduleBundler, runtimeEnvironment) {
53
- runtimeEnvironment.bundle = false;
37
+ async function ssrElement({specifier, props: templateProps}, moduleBundler, {runtimeEnvironment, runtimeParams}) {
54
38
  const {
55
39
  bundleRecord,
56
40
  code,
57
41
  specifier: bundleSpecifier,
58
42
  version
59
- } = await moduleBundler.getModuleBundle({specifier}, runtimeEnvironment, void 0, bundleConfigOverrides);
60
- const {error, result} = runtimeEnvironment.format === "amd" ? await runCodeOnWorker([
43
+ } = await moduleBundler.getModuleBundle({specifier}, {...runtimeEnvironment, bundle: false}, void 0, bundleConfigOverrides);
44
+ const context = {
45
+ props: templateProps,
46
+ params: runtimeParams.params || {},
47
+ query: runtimeParams.query || {},
48
+ locale: runtimeParams.locale || runtimeEnvironment.defaultLocale
49
+ };
50
+ const {error, result, props} = runtimeEnvironment.format === "amd" ? await (0, import_sandbox.default)([
61
51
  ...await (0, import_amd_utils.default)(runtimeEnvironment, version.replace(/\./g, "_"), bundleRecord.includedModules.find((m) => m.startsWith("lwc/v")), bundleSpecifier),
62
- `globalThis.ssrProps = ${JSON.stringify(props)};`,
63
52
  code
64
- ]) : await runCodeOnWorker([`globalThis.ssrProps = ${JSON.stringify(props)};`, code]);
53
+ ], context) : await (0, import_sandbox.default)([code], context);
65
54
  if (error) {
66
55
  throw new Error(error);
67
56
  } else {
68
- return result;
57
+ return {html: result, props};
69
58
  }
70
59
  }
@@ -1,2 +1,5 @@
1
1
  export declare const LWC_SSR_PREFIX = "@lwrjs/lwc-ssr/";
2
+ export declare const SSR_PROPS_ATTR = "data-lwr-props-id";
3
+ export declare const SSR_PROPS_KEY = "ssrProps";
4
+ export declare function getPropsId(): string;
2
5
  //# sourceMappingURL=identity.d.ts.map
@@ -1,2 +1,7 @@
1
1
  export const LWC_SSR_PREFIX = '@lwrjs/lwc-ssr/';
2
+ export const SSR_PROPS_ATTR = 'data-lwr-props-id';
3
+ export const SSR_PROPS_KEY = 'ssrProps';
4
+ export function getPropsId() {
5
+ return `lwcprops${Math.floor(Math.random() * 0x10000).toString(16)}`;
6
+ }
2
7
  //# sourceMappingURL=identity.js.map
@@ -6,15 +6,13 @@ import { ModuleCompiled, ModuleEntry, ModuleProvider, ProviderContext, AbstractM
6
6
  */
7
7
  /**
8
8
  * Create the virtual source for a module which server-side renders the given component.
9
- * This code is meant to be executed on the server; it is run in a worker in "lwc-ssr/viewTransformer#ssr-element" during linking.
10
- * The result is posted to the parentPort (via globalThis.postback() defined in the worker) and the Promise in the main thread resolves.
11
- * The properties are stored in "globalThis.ssrProps" and used in the SSR module.
12
- * The postback function "globalThis.postback" must be set prior to executing the SSR module.
13
- * @param rootComponent - The specifier for the component to SSR
14
- * @param globalThis.ssrProps - The properties to pass to the component, set in "lwc-ssr/viewTransformer#ssr-element"
9
+ * This code is meant to be executed in a worker on the server; it is run from "lwc-ssr/viewTransformer#ssr-element" during linking.
10
+ * The result is posted to the parentPort and the Promise in the main thread resolves.
11
+ * If available, getProps() is called on the root component to mutate context.props
12
+ * @param rootSpecifier - The specifier for the component to SSR
15
13
  * @returns the generated module source
16
14
  */
17
- export declare function createSsrBootstrapModule(rootComponent: string): string;
15
+ export declare function createSsrBootstrapModule(rootSpecifier: string): string;
18
16
  export default class LwcSsrModuleProvider implements ModuleProvider {
19
17
  name: string;
20
18
  version: string;
@@ -7,26 +7,36 @@ import { LWC_SSR_PREFIX } from '../identity.js';
7
7
  */
8
8
  /**
9
9
  * Create the virtual source for a module which server-side renders the given component.
10
- * This code is meant to be executed on the server; it is run in a worker in "lwc-ssr/viewTransformer#ssr-element" during linking.
11
- * The result is posted to the parentPort (via globalThis.postback() defined in the worker) and the Promise in the main thread resolves.
12
- * The properties are stored in "globalThis.ssrProps" and used in the SSR module.
13
- * The postback function "globalThis.postback" must be set prior to executing the SSR module.
14
- * @param rootComponent - The specifier for the component to SSR
15
- * @param globalThis.ssrProps - The properties to pass to the component, set in "lwc-ssr/viewTransformer#ssr-element"
10
+ * This code is meant to be executed in a worker on the server; it is run from "lwc-ssr/viewTransformer#ssr-element" during linking.
11
+ * The result is posted to the parentPort and the Promise in the main thread resolves.
12
+ * If available, getProps() is called on the root component to mutate context.props
13
+ * @param rootSpecifier - The specifier for the component to SSR
16
14
  * @returns the generated module source
17
15
  */
18
- export function createSsrBootstrapModule(rootComponent) {
19
- return [
20
- '/* This module is generated and meant to be used in a Server context */',
21
- 'import { renderComponent } from "@lwc/engine-server"',
22
- `import Ctor from "${rootComponent}"`,
23
- 'try {',
24
- ` const result = renderComponent("${moduleSpecifierToKebabCase(rootComponent)}", Ctor, globalThis.ssrProps);`,
25
- ' globalThis.postback({ result });',
26
- '} catch(err) {',
27
- ' globalThis.postback({ error: err.toString() });',
28
- `}`,
29
- ].join('\n');
16
+ export function createSsrBootstrapModule(rootSpecifier) {
17
+ return `
18
+ import { renderComponent } from '@lwc/engine-server';
19
+ import Ctor, * as rootComponent from '${rootSpecifier}';
20
+
21
+ (async () => {
22
+ try {
23
+ let props = globalThis.context.props;
24
+ if (rootComponent.getProps) {
25
+ const data = await rootComponent.getProps({
26
+ props,
27
+ params: globalThis.context.params,
28
+ query: globalThis.context.query,
29
+ locale: globalThis.context.locale,
30
+ fetch: globalThis.fetch,
31
+ });
32
+ props = data.props; // overwrite public props
33
+ }
34
+ const result = renderComponent('${moduleSpecifierToKebabCase(rootSpecifier)}', Ctor, props || {});
35
+ globalThis.postMessage({ result, props });
36
+ } catch(e) {
37
+ globalThis.postMessage({ error: e });
38
+ }
39
+ })()`;
30
40
  }
31
41
  export default class LwcSsrModuleProvider {
32
42
  constructor(providerConfig, { runtimeEnvironment: { lwrVersion } }) {
@@ -12,9 +12,11 @@ interface SsrPluginOptions {
12
12
  * 3. This view transformer links the SSRed string for EVERY custom element (ie: root component) found in the page document:
13
13
  * a) It requests a module which SSRs a given custom element, generated by "lwc-ssr/moduleProvider"
14
14
  * b) A bundle is created for the generated SSR module (see "./ssr-element")
15
- * c) The bundle code is run inside a worker (see "./ssr-element"), with properties stored in "globalThis.ssrProps"
16
- * d) The generated SSR module (running the worker) passes the SSRed code string back to the main thread (ie: "globalThis.ssrWorkerResult")
17
- * e) The SSRed string is used to overwrite/link each custom element (eg: "<c-app></c-app>") in the document (see "stringBuild.overwrite")
15
+ * c) The bundle code is run inside a worker (see "./ssr-element"), with context stored in "workerData"
16
+ * d) RootComponent.getProps() is run to preload data, if available
17
+ * e) The generated SSR module (running the worker) passes the SSRed code string back to the main thread
18
+ * f) The SSRed string is used to overwrite/link each custom element (eg: "<c-app></c-app>") in the document (see "stringBuilder.overwrite")
19
+ * g) A script containing all the serialized properties is added for hydration
18
20
  * 4. The view/page document now contains SSRed components, which will be sent to the client
19
21
  * 5. During bootstrap on the client, the "lwr/initSsr" module will hydrate ALL the custom elements on the page
20
22
  */
@@ -1,5 +1,5 @@
1
1
  import { kebabCaseToModuleSpecifer } from '@lwrjs/shared-utils';
2
- import { LWC_SSR_PREFIX } from '../identity.js';
2
+ import { LWC_SSR_PREFIX, SSR_PROPS_ATTR, SSR_PROPS_KEY, getPropsId } from '../identity.js';
3
3
  import { ssrElement } from './ssr-element.js';
4
4
  /**
5
5
  * This is a view transformer run by the view registry during linking of a page document/route (configured in lwr.config.json[routes]).
@@ -11,9 +11,11 @@ import { ssrElement } from './ssr-element.js';
11
11
  * 3. This view transformer links the SSRed string for EVERY custom element (ie: root component) found in the page document:
12
12
  * a) It requests a module which SSRs a given custom element, generated by "lwc-ssr/moduleProvider"
13
13
  * b) A bundle is created for the generated SSR module (see "./ssr-element")
14
- * c) The bundle code is run inside a worker (see "./ssr-element"), with properties stored in "globalThis.ssrProps"
15
- * d) The generated SSR module (running the worker) passes the SSRed code string back to the main thread (ie: "globalThis.ssrWorkerResult")
16
- * e) The SSRed string is used to overwrite/link each custom element (eg: "<c-app></c-app>") in the document (see "stringBuild.overwrite")
14
+ * c) The bundle code is run inside a worker (see "./ssr-element"), with context stored in "workerData"
15
+ * d) RootComponent.getProps() is run to preload data, if available
16
+ * e) The generated SSR module (running the worker) passes the SSRed code string back to the main thread
17
+ * f) The SSRed string is used to overwrite/link each custom element (eg: "<c-app></c-app>") in the document (see "stringBuilder.overwrite")
18
+ * g) A script containing all the serialized properties is added for hydration
17
19
  * 4. The view/page document now contains SSRed components, which will be sent to the client
18
20
  * 5. During bootstrap on the client, the "lwr/initSsr" module will hydrate ALL the custom elements on the page
19
21
  */
@@ -27,8 +29,9 @@ export default function lwcSsrViewTranformer(options, { moduleBundler }) {
27
29
  return;
28
30
  }
29
31
  if (viewContext.view.bootstrap?.experimentalSSR) {
32
+ // Gather all the SSRable custom elements (ie: root components) into 1 list
30
33
  const ssrModules = [];
31
- for (const { tagName, location, props = {} } of customElements) {
34
+ for (const { tagName, location, props } of customElements) {
32
35
  if (location) {
33
36
  const { startOffset, endOffset } = location;
34
37
  const moduleSpecifier = kebabCaseToModuleSpecifer(tagName);
@@ -36,15 +39,32 @@ export default function lwcSsrViewTranformer(options, { moduleBundler }) {
36
39
  startOffset,
37
40
  endOffset,
38
41
  props,
42
+ tagName,
39
43
  specifier: `${LWC_SSR_PREFIX}${moduleSpecifier}`,
40
44
  });
41
45
  }
42
46
  }
43
- return Promise.all(ssrModules.map(({ specifier, props, startOffset, endOffset }) => {
44
- return ssrElement({ specifier, props }, moduleBundler, viewContext.runtimeEnvironment).then((ssrResult) => {
45
- stringBuilder.overwrite(startOffset, endOffset, ssrResult);
47
+ // SSR and gather the properties for each eligible custom element, in parallel
48
+ const ssrProps = {};
49
+ await Promise.all(ssrModules.map(({ specifier, tagName, props, startOffset, endOffset }) => {
50
+ return ssrElement({ specifier, props }, moduleBundler, viewContext).then(({ html, props }) => {
51
+ if (props) {
52
+ // Add the props id to the HTML for the custom element
53
+ // eg: <some-cmp> -> <some-cmp data-lwr-props-id="1234">
54
+ const propsId = getPropsId();
55
+ ssrProps[propsId] = props;
56
+ const [, remain] = html.split(`<${tagName}`);
57
+ html = [`<${tagName}`, ` ${SSR_PROPS_ATTR}="${propsId}"`, remain].join('');
58
+ }
59
+ // Overwrite the custom element with the SSRed component string
60
+ stringBuilder.overwrite(startOffset, endOffset, html);
46
61
  });
47
- })).then(() => undefined); // we need to ultimately return "void" here, there is nothing to return
62
+ }));
63
+ if (Object.keys(ssrProps).length) {
64
+ // Serialize all root component properties into a single script for the page
65
+ // Append the script before the custom elements; it MUST appear before the AMD shim to avoid timing issues
66
+ stringBuilder.prependLeft(ssrModules[0].startOffset, `<script type="application/javascript">globalThis.LWR = globalThis.LWR || {};globalThis.LWR.${SSR_PROPS_KEY} = ${JSON.stringify(ssrProps)};</script>`);
67
+ }
48
68
  }
49
69
  },
50
70
  };
@@ -0,0 +1,9 @@
1
+ import { Json, PropsRequestContext } from '@lwrjs/types';
2
+ interface SandboxResults {
3
+ result?: string;
4
+ error?: string;
5
+ props?: Json;
6
+ }
7
+ export default function runCode(codes: string[], context: Partial<PropsRequestContext>): Promise<SandboxResults>;
8
+ export {};
9
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { Worker } from 'worker_threads';
2
+ const HEADER = '/* This module is generated and meant to be used in a Server context */';
3
+ /**
4
+ * Sandbox APIs
5
+ * @param globalThis.context
6
+ * context such as props, params, and locale for executing SSR
7
+ *
8
+ * @param globalThis.fetch
9
+ * fetch function for external data fetching
10
+ *
11
+ * @param globalThis.postMessage
12
+ * postbacks from sandbox to main thread
13
+ */
14
+ const WORKER_CODE_SANDBOX_APIS = [
15
+ `const { parentPort, workerData } = require('worker_threads');`,
16
+ `globalThis.context = workerData;`,
17
+ `globalThis.fetch = require('node-fetch');`,
18
+ `globalThis.postMessage = (...args) => parentPort.postMessage(...args);`,
19
+ ];
20
+ /**
21
+ * Run the SSR module code in a worker, and return the results to the main thread.
22
+ * @param codes - Code strings which SSR a root component
23
+ * @returns a promise to the SSRed code string, or an error message
24
+ */
25
+ function runCodeOnWorker(codes, workerData) {
26
+ const workerCode = [HEADER, ...WORKER_CODE_SANDBOX_APIS, ...codes].join('\n');
27
+ return new Promise((resolve) => {
28
+ const worker = new Worker(workerCode, { eval: true, workerData });
29
+ worker.on('message', resolve);
30
+ worker.on('error', ({ message }) => resolve({ error: `SSR worker exited with error: ${message}` }));
31
+ worker.on('exit', (code) => {
32
+ if (code !== 0) {
33
+ resolve({ error: `SSR worker stopped with exit code: ${code}` });
34
+ }
35
+ });
36
+ });
37
+ }
38
+ export default function runCode(codes, context) {
39
+ return runCodeOnWorker(codes, context);
40
+ }
41
+ //# sourceMappingURL=sandbox.js.map
@@ -1,16 +1,17 @@
1
- import type { ModuleBundler, RuntimeEnvironment } from '@lwrjs/types';
1
+ import type { Json, ModuleBundler, ViewTranformPluginContext } from '@lwrjs/types';
2
2
  /**
3
- * Create a bundle for the given SSR module and run the code in a worker.
3
+ * Create a bundle for the given SSR module and run the code in a sandbox.
4
4
  * @param moduleInfo - specifier: The ID of the module, generated by "lwc-ssr/moduleProvider", which SSRs a component
5
5
  * props: A map of the key:value property pairs parsed from the custom element attributes (ie: all string values)
6
6
  * @param moduleBundler
7
7
  * @param runtimeEnvironment
8
8
  * @returns a promise to the SSRed code string
9
9
  */
10
- export declare function ssrElement<RuntimeEnv extends RuntimeEnvironment>({ specifier, props }: {
10
+ export declare function ssrElement({ specifier, props: templateProps }: {
11
11
  specifier: string;
12
- props: {
13
- [name: string]: string;
14
- };
15
- }, moduleBundler: ModuleBundler, runtimeEnvironment: RuntimeEnv): Promise<string>;
12
+ props: Json;
13
+ }, moduleBundler: ModuleBundler, { runtimeEnvironment, runtimeParams }: ViewTranformPluginContext): Promise<{
14
+ html: string;
15
+ props: Json;
16
+ }>;
16
17
  //# sourceMappingURL=ssr-element.d.ts.map
@@ -1,59 +1,44 @@
1
- import { Worker } from 'worker_threads';
2
1
  import getCode from './amd-utils.js';
2
+ import runCode from './sandbox.js';
3
3
  const bundleConfigOverrides = {
4
4
  exclude: [],
5
5
  alias: {
6
6
  lwc: '@lwc/engine-server', // override the default "@lwc/engine-dom" package
7
7
  },
8
8
  };
9
- const WORKER_CONTEXT_PARENTPORT = `const { parentPort } = require('worker_threads');`;
10
- const WORKER_CONTEXT_POSTBACK = `globalThis.postback = (result) => parentPort.postMessage(result);`;
11
9
  /**
12
- * Run the SSR process in a worker, and return the results to the main thread.
13
- * The "globalThis.postback" is a function to allow SSR module to post results back to the parentPort, and the Promise in the main thread resolves.
14
- * The properties are stored in "globalThis.ssrProps" and used in the SSR module.
15
- * @param codes - Code strings which SSR a root component
16
- * @returns a promise to the SSRed code string, or an error message
17
- */
18
- function runCodeOnWorker(codes) {
19
- const workerCode = [WORKER_CONTEXT_PARENTPORT, WORKER_CONTEXT_POSTBACK, ...codes].join('\n');
20
- return new Promise((resolve) => {
21
- const worker = new Worker(workerCode, { eval: true });
22
- worker.on('message', resolve);
23
- worker.on('error', ({ message }) => resolve({ error: `SSR worker exited with error: ${message}` }));
24
- worker.on('exit', (code) => {
25
- if (code !== 0) {
26
- resolve({ error: `SSR worker stopped with exit code: ${code}` });
27
- }
28
- });
29
- });
30
- }
31
- /**
32
- * Create a bundle for the given SSR module and run the code in a worker.
10
+ * Create a bundle for the given SSR module and run the code in a sandbox.
33
11
  * @param moduleInfo - specifier: The ID of the module, generated by "lwc-ssr/moduleProvider", which SSRs a component
34
12
  * props: A map of the key:value property pairs parsed from the custom element attributes (ie: all string values)
35
13
  * @param moduleBundler
36
14
  * @param runtimeEnvironment
37
15
  * @returns a promise to the SSRed code string
38
16
  */
39
- export async function ssrElement({ specifier, props }, moduleBundler, runtimeEnvironment) {
17
+ export async function ssrElement({ specifier, props: templateProps }, moduleBundler, { runtimeEnvironment, runtimeParams }) {
18
+ const { bundleRecord, code, specifier: bundleSpecifier, version, } = await moduleBundler.getModuleBundle({ specifier },
40
19
  // Ensure the bundle flag is always off,
41
20
  // otherwise TOO much gets bundled in the module registry
42
21
  // in ESM, resulting lwc clashes/duplication
43
- runtimeEnvironment.bundle = false;
44
- const { bundleRecord, code, specifier: bundleSpecifier, version, } = await moduleBundler.getModuleBundle({ specifier }, runtimeEnvironment, undefined, bundleConfigOverrides);
45
- const { error, result } = runtimeEnvironment.format === 'amd'
46
- ? await runCodeOnWorker([
22
+ { ...runtimeEnvironment, bundle: false }, undefined, bundleConfigOverrides);
23
+ // Gather context to send into the SSR sandbox
24
+ const context = {
25
+ props: templateProps,
26
+ params: runtimeParams.params || {},
27
+ query: runtimeParams.query || {},
28
+ locale: runtimeParams.locale || runtimeEnvironment.defaultLocale,
29
+ };
30
+ // Get the SSR string and properties bag
31
+ const { error, result, props } = runtimeEnvironment.format === 'amd'
32
+ ? await runCode([
47
33
  ...(await getCode(runtimeEnvironment, version.replace(/\./g, '_'), bundleRecord.includedModules.find((m) => m.startsWith('lwc/v')), bundleSpecifier)),
48
- `globalThis.ssrProps = ${JSON.stringify(props)};`,
49
34
  code,
50
- ])
51
- : await runCodeOnWorker([`globalThis.ssrProps = ${JSON.stringify(props)};`, code]);
35
+ ], context)
36
+ : await runCode([code], context);
52
37
  if (error) {
53
38
  throw new Error(error);
54
39
  }
55
40
  else {
56
- return result;
41
+ return { html: result, props };
57
42
  }
58
43
  }
59
44
  //# sourceMappingURL=ssr-element.js.map
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.7.0-alpha.9",
7
+ "version": "0.7.2",
8
8
  "homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
9
9
  "repository": {
10
10
  "type": "git",
@@ -33,14 +33,14 @@
33
33
  "build/**/*.d.ts"
34
34
  ],
35
35
  "dependencies": {
36
- "@lwrjs/diagnostics": "0.7.0-alpha.9",
37
- "@lwrjs/shared-utils": "0.7.0-alpha.9"
36
+ "@lwrjs/diagnostics": "0.7.2",
37
+ "@lwrjs/shared-utils": "0.7.2"
38
38
  },
39
39
  "devDependencies": {
40
- "@lwrjs/types": "0.7.0-alpha.9"
40
+ "@lwrjs/types": "0.7.2"
41
41
  },
42
42
  "engines": {
43
- "node": ">=14.15.4 <17"
43
+ "node": ">=14.15.4 <19"
44
44
  },
45
- "gitHead": "522665298cc74f1898da6ed018fa72504bd72645"
45
+ "gitHead": "1467a95aa5dcf6901e1122f6025ffd4f22237a3f"
46
46
  }