@lwrjs/lwc-ssr 0.8.0-alpha.8 → 0.9.0-alpha.0

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 CHANGED
@@ -41,7 +41,7 @@ SSR is activated on a per-route basis by changing `bootstrap.ssr` to `true`:
41
41
 
42
42
  ### Building SSR pages
43
43
 
44
- When a route with `bootstrap.ssr` 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()`.
44
+ When a route with `bootstrap.ssr` 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
45
 
46
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
47
 
@@ -57,72 +57,122 @@ LWR will automatically pass any root component attributes from a [template](http
57
57
 
58
58
  #### Limitations
59
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.
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. This code must be **portable** or SSR will fail.
61
61
 
62
- Because of this, the [`@lwrjs/router`](https://github.com/salesforce/lwr-recipes/blob/main/doc/navigation.md) is not supported with SSR.
62
+ > Code is _portable_ when it can run in a headless environment, where there is no access to DOM APIs (eg: window).
63
63
 
64
- ### Preloading data during SSR
64
+ ### Loading data during SSR
65
65
 
66
- Many components depend on external data. LWR provides a `getProps()` hook for developers to fetch that data on the server. SSR Sandbox supports `fetch` with its `globalThis` so developers could use `fetch` as if the module is executed by the browser. LWR passes the data to the component during SSR as [properties](<(https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public)>).
66
+ Many components depend on external data and resources. LWR provides a `getPageData()` hook for developers to fetch data on the server. During SSR, LWR calls the `getPageData()` hook for each **root component**, then serializes the resulting data into the page document as either [JSON](#json) or [markup](#markup).
67
67
 
68
- > **Important**: This hook is **only** run for root components.
68
+ > **Important**: `getPageData()` is **only** run for root components.
69
69
 
70
- The `getProps()` hook is exported as a function from a root component module:
70
+ ```ts
71
+ type GetPageDataHook = (context: SsrRequestContext) => Promise<PageDataResponse>;
72
+
73
+ interface SsrRequestContext {
74
+ // existing props from template attributes
75
+ props: Json;
76
+ // values from a parameterized route defined in lwr.config.json
77
+ params: { [key: string]: string };
78
+ // search parameters from the request URL
79
+ query: { [key: string]: string };
80
+ // locale string for the request, eg: 'en-US'
81
+ locale: string;
82
+ }
83
+
84
+ interface PageDataResponse {
85
+ props?: Json; // JSON serializable properties for the root component
86
+ markup?: {
87
+ // HTML serializable data
88
+ links?: {
89
+ // links to contribute to the <head> markup
90
+ href: string;
91
+ as?: string;
92
+ rel?: string;
93
+ fetchpriority?: 'high' | 'low' | 'auto';
94
+ }[];
95
+ };
96
+ }
97
+
98
+ type Json = undefined | null | boolean | number | string | Json[] | { [prop: string]: Json };
99
+ ```
100
+
101
+ In LWR, the SSR process runs in a sandbox. This sandbox supports `globalThis.fetch` so developers can use `fetch()` in their `getPageData()` hooks as if the module is being executed in browser.
102
+
103
+ #### JSON
104
+
105
+ Data in `PageDataResponse.props` is serialized into the page document as JSON. LWR passes this data to the component as [public properties](<(https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public)>) during both SSR and [client hydration](#client-hydration).
106
+
107
+ #### Markup
108
+
109
+ Data in `PageDataResponse.markup` is serialized into the page document as HTML. LWR adds each `markup.link` returned by `getPageData()` to the `<head>` section of the page document. For example, developers can [preload images](https://developer.chrome.com/blog/link-rel-preload/) to improve a page's performance on the client.
110
+
111
+ #### Example
112
+
113
+ The `getPageData()` hook is exported as a function from a root component module:
71
114
 
72
115
  ```ts
73
116
  // my-app/src/modules/my/root/root.ts
74
117
  import { LightningElement, api } from 'lwc';
75
- import type { PropsRequestContext, PropsResponse } from '@lwrjs/types';
118
+ import type { SsrRequestContext, PageDataResponse } from '@lwrjs/types';
76
119
 
77
120
  export default class MyRoot extends LightningElement {
78
121
  @api data: SomeDataType[] = [];
79
122
  }
80
123
 
81
- export async function getProps(context: PropsRequestContext): Promise<PropsResponse> {
124
+ export async function getPageData(context: SsrRequestContext): Promise<PageDataResponse> {
82
125
  // "/category/books" => context.params = { category: 'books' }
83
126
  const category = context.params.category;
127
+
84
128
  // page.html template => context.props = { limit: '10' }
85
129
  const num = context.props.limit || '25';
86
130
  const res = await fetch(`https://www.some-api.com/${category}?lang=${context.locale}&num=${num}`);
87
131
  const data = await res.json();
132
+
88
133
  return {
89
134
  props: {
90
135
  // will be passed to the root component as props
91
136
  data,
92
137
  ...context.props, // pass the template props through, if desired
93
138
  },
139
+ markup: {
140
+ links: [
141
+ {
142
+ // preload an important image to boost performance
143
+ href: data.thumbnail,
144
+ as: 'image',
145
+ rel: 'preload',
146
+ fetchpriority: 'high',
147
+ },
148
+ ],
149
+ },
94
150
  };
95
151
  }
96
152
  ```
97
153
 
98
- ```ts
99
- type GetPropsHook = (context: PropsRequestContext) => Promise<PropsResponse>;
154
+ Notes:
100
155
 
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
- }
156
+ - The `getPageData()` hook can choose to merge the properties from `SsrRequestContext.props` into its return object, or it can ignore/discard them.
157
+ - The author of `getPageData()` is responsible for validating the `params` and `query` from `SsrRequestContext` before using them.
158
+ - The **same** `props` returned by `getPageData()` are passed to the component during server rendering **and** client hydration.
111
159
 
112
- interface PropsResponse {
113
- // serializable props for the root component
114
- props: Json;
115
- }
160
+ ### Client hydration
116
161
 
117
- type Json = undefined | null | boolean | number | string | Json[] | { [prop: string]: Json };
118
- ```
162
+ 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.
119
163
 
120
- Notes:
164
+ ### Debugging
121
165
 
122
- - The `getProps()` hook can choose to merge the properties from `PropsRequestContext.props` into its return object, or it can ignore/discard them.
123
- - The author of `getProps()` is responsible for validating the `params` and `query` from `PropsRequestContext` before using them.
124
- - The **same** `props` returned by `getProps()` are passed to the component during server rendering **and** client hydration.
166
+ With the introduction of [@locker/near-membrane-node](https://github.com/salesforce/near-membrane/tree/main/packages/near-membrane-node) sandbox, debugging a module in server side rendering is straightforward:
125
167
 
126
- ### Client hydration
168
+ 1. setting a `debugger;` statement in your module
169
+ 2. attach node process to the LWR server
127
170
 
128
- 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.
171
+ ```sh
172
+ LOCKER_SB=true yarn lwr:example:debug
173
+ ```
174
+
175
+ 3. visit intended url(http://localhost:3000/ssr in this example) with a browser
176
+ 4. when `debugger;` statement is executed, VS Code would break with the VM tab showing up
177
+
178
+ ![Sandbox Debugging Screenshot](../../../assets/sandbox-debug.png)
@@ -35,19 +35,21 @@ import { renderComponent } from '@lwc/engine-server';
35
35
  import Ctor, * as rootComponent from '${rootSpecifier}';
36
36
 
37
37
  (async () => {
38
- // 1. setup props
38
+ // 1. setup page data
39
39
  const context = globalThis.getContext();
40
40
  let props = context.props;
41
- if (rootComponent.getProps) {
42
- const data = await rootComponent.getProps(context);
41
+ let markup;
42
+ if (rootComponent.getPageData) {
43
+ const data = await rootComponent.getPageData(context);
43
44
  props = data.props; // overwrite public props
45
+ markup = data.markup;
44
46
  }
45
47
 
46
48
  // 2. render component
47
49
  const result = renderComponent('${(0, import_shared_utils.moduleSpecifierToKebabCase)(rootSpecifier)}', Ctor, props || {});
48
50
 
49
51
  // 3. relay results
50
- globalThis.postMessage({ result, props });
52
+ globalThis.resolver({ result, props, markup });
51
53
  })()`;
52
54
  }
53
55
  var LwcSsrModuleProvider = class {
@@ -54,22 +54,36 @@ function lwcSsrViewTranformer(options, {moduleBundler}) {
54
54
  }
55
55
  }
56
56
  const ssrProps = {};
57
+ let ssrLinks = "";
57
58
  await Promise.all(ssrModules.map(({specifier, tagName, props, startOffset, endOffset}) => {
58
- return (0, import_ssr_element.ssrElement)({specifier, props}, moduleBundler, viewContext).then(({html, props: props2}) => {
59
+ return (0, import_ssr_element.ssrElement)({specifier, props}, moduleBundler, viewContext).then(({html, props: props2, markup: {links = []} = {links: []}}) => {
59
60
  if (props2) {
60
61
  const propsId = (0, import_identity.getPropsId)();
61
62
  ssrProps[propsId] = props2;
62
63
  const [, remain] = html.split(`<${tagName}`);
63
64
  html = [`<${tagName}`, ` ${import_identity.SSR_PROPS_ATTR}="${propsId}"`, remain].join("");
64
65
  }
65
- if (html.length > 0) {
66
- stringBuilder.overwrite(startOffset, endOffset, html);
67
- }
66
+ links.forEach(({href, rel, as, fetchpriority}) => {
67
+ const relStr = rel ? ` rel="${rel}"` : "", asStr = as ? ` as="${as}"` : "", fetchStr = fetchpriority ? ` fetchpriority="${fetchpriority}"` : "";
68
+ ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>
69
+ `;
70
+ });
71
+ stringBuilder.overwrite(startOffset, endOffset, html);
72
+ }).catch((err) => {
73
+ import_shared_utils.logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
68
74
  });
69
75
  }));
70
76
  if (Object.keys(ssrProps).length) {
71
77
  stringBuilder.prependLeft(ssrModules[0].startOffset, `<script type="application/javascript">globalThis.LWR = globalThis.LWR || {};globalThis.LWR.${import_identity.SSR_PROPS_KEY} = ${JSON.stringify(ssrProps)};</script>`);
72
78
  }
79
+ if (ssrLinks) {
80
+ const headIndex = stringBuilder.original.indexOf("</head>");
81
+ if (headIndex >= 0) {
82
+ stringBuilder.prependLeft(headIndex, ssrLinks);
83
+ } else {
84
+ import_shared_utils.logger.error("Adding links during server-side rendering failed. Could not find the </head> tag.");
85
+ }
86
+ }
73
87
  import_shared_utils.logger.verbose("lwcSsrViewTranformer response", stringBuilder);
74
88
  }
75
89
  }
@@ -0,0 +1,54 @@
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-locker.ts
25
+ __markAsModule(exports);
26
+ __export(exports, {
27
+ default: () => runCode
28
+ });
29
+ var import_near_membrane_node = __toModule(require("@locker/near-membrane-node"));
30
+ var import_node_fetch = __toModule(require("node-fetch"));
31
+ function runCode(codes, context) {
32
+ return new Promise((resolve, reject) => {
33
+ let resolver;
34
+ const p = new Promise((r) => resolver = r);
35
+ function getContext() {
36
+ return context;
37
+ }
38
+ const endowments = Object.getOwnPropertyDescriptors({
39
+ getContext,
40
+ fetch: import_node_fetch.default,
41
+ resolver,
42
+ process,
43
+ setTimeout,
44
+ clearTimeout
45
+ });
46
+ const ve = (0, import_near_membrane_node.default)(globalThis, {endowments});
47
+ try {
48
+ ve.evaluate(codes.join("\n"));
49
+ p.then(resolve);
50
+ } catch (e) {
51
+ reject(e);
52
+ }
53
+ });
54
+ }
@@ -0,0 +1,49 @@
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-worker.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.getContext = () => workerData;`,
34
+ `globalThis.fetch = require('node-fetch');`,
35
+ `globalThis.resolver = (...args) => parentPort.postMessage(...args);`
36
+ ];
37
+ function runCode(codes, workerData) {
38
+ const workerCode = [HEADER, ...WORKER_CODE_SANDBOX_APIS, ...codes].join("\n");
39
+ return new Promise((resolve, reject) => {
40
+ const worker = new import_worker_threads.Worker(workerCode, {eval: true, workerData});
41
+ worker.on("message", resolve);
42
+ worker.on("error", reject);
43
+ worker.on("exit", (code) => {
44
+ if (code !== 0) {
45
+ reject(new Error(`SSR worker stopped with exit code: ${code}`));
46
+ }
47
+ });
48
+ });
49
+ }
@@ -26,28 +26,11 @@ __markAsModule(exports);
26
26
  __export(exports, {
27
27
  default: () => runCode
28
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.getContext = () => 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", (err) => resolve({error: `SSR worker exited with error: ${err.message}
43
- ${err.stack}`}));
44
- worker.on("exit", (code) => {
45
- if (code !== 0) {
46
- resolve({error: `SSR worker stopped with exit code: ${code}`});
47
- }
48
- });
49
- });
50
- }
29
+ var import_sandbox_worker = __toModule(require("./sandbox-worker.cjs"));
30
+ var import_sandbox_locker = __toModule(require("./sandbox-locker.cjs"));
51
31
  function runCode(codes, context) {
52
- return runCodeOnWorker(codes, context);
32
+ if (process.env.LOCKER_SB === "true") {
33
+ return (0, import_sandbox_locker.default)(codes, context);
34
+ }
35
+ return (0, import_sandbox_worker.default)(codes, context);
53
36
  }
@@ -28,34 +28,35 @@ __export(exports, {
28
28
  });
29
29
  var import_amd_utils = __toModule(require("./amd-utils.cjs"));
30
30
  var import_sandbox = __toModule(require("./sandbox.cjs"));
31
- var bundleConfigOverrides = {
32
- exclude: [],
33
- alias: {
34
- lwc: "@lwc/engine-server"
35
- }
36
- };
31
+ var import_perf_hooks = __toModule(require("perf_hooks"));
37
32
  async function ssrElement({specifier, props: templateProps}, moduleBundler, {runtimeEnvironment, runtimeParams}) {
38
- const {format, bundle} = runtimeEnvironment;
33
+ const {format} = runtimeEnvironment;
39
34
  const {
40
35
  bundleRecord,
41
36
  code,
42
37
  specifier: bundleSpecifier,
43
38
  version
44
- } = await moduleBundler.getModuleBundle({specifier}, {...runtimeEnvironment, bundle: format === "esm" ? false : bundle}, void 0, bundleConfigOverrides);
39
+ } = format === "esm" ? await moduleBundler.getModuleBundle({specifier}, {...runtimeEnvironment, bundle: false}, void 0, {
40
+ exclude: [],
41
+ alias: {
42
+ lwc: "@lwc/engine-server"
43
+ }
44
+ }) : await moduleBundler.getModuleBundle({specifier}, runtimeEnvironment, void 0, {
45
+ exclude: ["lwc"]
46
+ });
45
47
  const context = {
46
48
  props: templateProps,
47
49
  params: runtimeParams.params || {},
48
50
  query: runtimeParams.query || {},
49
51
  locale: runtimeParams.locale || runtimeEnvironment.defaultLocale
50
52
  };
51
- const {error, result, props} = runtimeEnvironment.format === "amd" ? await (0, import_sandbox.default)([
53
+ const startTime = import_perf_hooks.performance.now();
54
+ const {result, props, markup} = format === "amd" ? await (0, import_sandbox.default)([
52
55
  ...await (0, import_amd_utils.default)(runtimeEnvironment, version.replace(/\./g, "_"), bundleSpecifier, bundleRecord.includedModules),
53
56
  code
54
57
  ], context) : await (0, import_sandbox.default)([code], context);
55
- if (error) {
56
- console.error(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, error);
57
- return {html: "", props: void 0};
58
- } else {
59
- return {html: result, props};
60
- }
58
+ const endTime = import_perf_hooks.performance.now();
59
+ const timeDiff = endTime - startTime;
60
+ console.log(`[${specifier} SSR] complete in ${timeDiff} ms`);
61
+ return {html: result, props, markup};
61
62
  }
@@ -8,7 +8,7 @@ import { ModuleCompiled, ModuleEntry, ModuleProvider, ProviderContext, AbstractM
8
8
  * Create the virtual source for a module which server-side renders the given component.
9
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
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
11
+ * If available, getPageData() is called on the root component to mutate context.props and gather page markup
12
12
  * @param rootSpecifier - The specifier for the component to SSR
13
13
  * @returns the generated module source
14
14
  */
@@ -9,7 +9,7 @@ import { LWC_SSR_PREFIX } from '../identity.js';
9
9
  * Create the virtual source for a module which server-side renders the given component.
10
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
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
12
+ * If available, getPageData() is called on the root component to mutate context.props and gather page markup
13
13
  * @param rootSpecifier - The specifier for the component to SSR
14
14
  * @returns the generated module source
15
15
  */
@@ -19,19 +19,21 @@ import { renderComponent } from '@lwc/engine-server';
19
19
  import Ctor, * as rootComponent from '${rootSpecifier}';
20
20
 
21
21
  (async () => {
22
- // 1. setup props
22
+ // 1. setup page data
23
23
  const context = globalThis.getContext();
24
24
  let props = context.props;
25
- if (rootComponent.getProps) {
26
- const data = await rootComponent.getProps(context);
25
+ let markup;
26
+ if (rootComponent.getPageData) {
27
+ const data = await rootComponent.getPageData(context);
27
28
  props = data.props; // overwrite public props
29
+ markup = data.markup;
28
30
  }
29
31
 
30
32
  // 2. render component
31
33
  const result = renderComponent('${moduleSpecifierToKebabCase(rootSpecifier)}', Ctor, props || {});
32
34
 
33
35
  // 3. relay results
34
- globalThis.postMessage({ result, props });
36
+ globalThis.resolver({ result, props, markup });
35
37
  })()`;
36
38
  }
37
39
  export default class LwcSsrModuleProvider {
@@ -13,7 +13,7 @@ interface SsrPluginOptions {
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
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
16
+ * d) RootComponent.getPageData() is run to preload data and <links>, if available
17
17
  * e) The generated SSR module (running the worker) passes the SSRed code string back to the main thread
18
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
19
  * g) A script containing all the serialized properties is added for hydration
@@ -12,7 +12,7 @@ import { ssrElement } from './ssr-element.js';
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
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
15
+ * d) RootComponent.getPageData() is run to preload data and <links>, if available
16
16
  * e) The generated SSR module (running the worker) passes the SSRed code string back to the main thread
17
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
18
  * g) A script containing all the serialized properties is added for hydration
@@ -46,10 +46,12 @@ export default function lwcSsrViewTranformer(options, { moduleBundler }) {
46
46
  });
47
47
  }
48
48
  }
49
- // SSR and gather the properties for each eligible custom element, in parallel
49
+ // SSR and gather the properties and links for each eligible custom element, in parallel
50
50
  const ssrProps = {};
51
+ let ssrLinks = '';
51
52
  await Promise.all(ssrModules.map(({ specifier, tagName, props, startOffset, endOffset }) => {
52
- return ssrElement({ specifier, props }, moduleBundler, viewContext).then(({ html, props }) => {
53
+ return ssrElement({ specifier, props }, moduleBundler, viewContext)
54
+ .then(({ html, props, markup: { links = [] } = { links: [] } }) => {
53
55
  if (props) {
54
56
  // Add the props id to the HTML for the custom element
55
57
  // eg: <some-cmp> -> <some-cmp data-lwr-props-id="1234">
@@ -58,10 +60,16 @@ export default function lwcSsrViewTranformer(options, { moduleBundler }) {
58
60
  const [, remain] = html.split(`<${tagName}`);
59
61
  html = [`<${tagName}`, ` ${SSR_PROPS_ATTR}="${propsId}"`, remain].join('');
60
62
  }
61
- if (html.length > 0) {
62
- // Overwrite the custom element with the SSRed component string
63
- stringBuilder.overwrite(startOffset, endOffset, html);
64
- }
63
+ links.forEach(({ href, rel, as, fetchpriority }) => {
64
+ // Create HTML <link> strings for each item in the links array
65
+ const relStr = rel ? ` rel="${rel}"` : '', asStr = as ? ` as="${as}"` : '', fetchStr = fetchpriority ? ` fetchpriority="${fetchpriority}"` : '';
66
+ ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>\n`;
67
+ });
68
+ // Overwrite the custom element with the SSRed component string
69
+ stringBuilder.overwrite(startOffset, endOffset, html);
70
+ })
71
+ .catch((err) => {
72
+ logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
65
73
  });
66
74
  }));
67
75
  if (Object.keys(ssrProps).length) {
@@ -69,6 +77,16 @@ export default function lwcSsrViewTranformer(options, { moduleBundler }) {
69
77
  // Append the script before the custom elements; it MUST appear before the AMD shim to avoid timing issues
70
78
  stringBuilder.prependLeft(ssrModules[0].startOffset, `<script type="application/javascript">globalThis.LWR = globalThis.LWR || {};globalThis.LWR.${SSR_PROPS_KEY} = ${JSON.stringify(ssrProps)};</script>`);
71
79
  }
80
+ if (ssrLinks) {
81
+ // Add all the links to the <head> section of the base document
82
+ const headIndex = stringBuilder.original.indexOf('</head>');
83
+ if (headIndex >= 0) {
84
+ stringBuilder.prependLeft(headIndex, ssrLinks);
85
+ }
86
+ else {
87
+ logger.error('Adding links during server-side rendering failed. Could not find the </head> tag.');
88
+ }
89
+ }
72
90
  logger.verbose('lwcSsrViewTranformer response', stringBuilder);
73
91
  }
74
92
  },
@@ -0,0 +1,7 @@
1
+ import { PageDataResponse, SsrRequestContext } from '@lwrjs/types';
2
+ interface SandboxResults extends PageDataResponse {
3
+ result?: string;
4
+ }
5
+ export default function runCode(codes: string[], context: SsrRequestContext): Promise<SandboxResults>;
6
+ export {};
7
+ //# sourceMappingURL=sandbox-locker.d.ts.map
@@ -0,0 +1,29 @@
1
+ import createVirtualEnvironment from '@locker/near-membrane-node';
2
+ import fetch from 'node-fetch';
3
+ export default function runCode(codes, context) {
4
+ return new Promise((resolve, reject) => {
5
+ let resolver;
6
+ const p = new Promise((r) => (resolver = r));
7
+ function getContext() {
8
+ return context;
9
+ }
10
+ const endowments = Object.getOwnPropertyDescriptors({
11
+ getContext,
12
+ fetch,
13
+ resolver,
14
+ // for AMD loader ModuleRegistry
15
+ process,
16
+ setTimeout,
17
+ clearTimeout,
18
+ });
19
+ const ve = createVirtualEnvironment(globalThis, { endowments });
20
+ try {
21
+ ve.evaluate(codes.join('\n'));
22
+ p.then(resolve);
23
+ }
24
+ catch (e) {
25
+ reject(e);
26
+ }
27
+ });
28
+ }
29
+ //# sourceMappingURL=sandbox-locker.js.map
@@ -0,0 +1,12 @@
1
+ import { PageDataResponse, SsrRequestContext } from '@lwrjs/types';
2
+ interface SandboxResults extends PageDataResponse {
3
+ result?: string;
4
+ }
5
+ /**
6
+ * Run the SSR module code in a worker, and return the results to the main thread.
7
+ * @param codes - Code strings which SSR a root component
8
+ * @returns a promise to the SSRed code string, or an error message
9
+ */
10
+ export default function runCode(codes: string[], workerData: SsrRequestContext): Promise<SandboxResults>;
11
+ export {};
12
+ //# sourceMappingURL=sandbox-worker.d.ts.map
@@ -0,0 +1,38 @@
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.getContext
6
+ * function to provide prop request context object with properties such as props, params, and locale
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.getContext = () => workerData;`,
17
+ `globalThis.fetch = require('node-fetch');`,
18
+ `globalThis.resolver = (...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
+ export default function runCode(codes, workerData) {
26
+ const workerCode = [HEADER, ...WORKER_CODE_SANDBOX_APIS, ...codes].join('\n');
27
+ return new Promise((resolve, reject) => {
28
+ const worker = new Worker(workerCode, { eval: true, workerData });
29
+ worker.on('message', resolve);
30
+ worker.on('error', reject);
31
+ worker.on('exit', (code) => {
32
+ if (code !== 0) {
33
+ reject(new Error(`SSR worker stopped with exit code: ${code}`));
34
+ }
35
+ });
36
+ });
37
+ }
38
+ //# sourceMappingURL=sandbox-worker.js.map
@@ -1,9 +1,7 @@
1
- import { Json, PropsRequestContext } from '@lwrjs/types';
2
- interface SandboxResults {
1
+ import { PageDataResponse, SsrRequestContext } from '@lwrjs/types';
2
+ interface SandboxResults extends PageDataResponse {
3
3
  result?: string;
4
- error?: string;
5
- props?: Json;
6
4
  }
7
- export default function runCode(codes: string[], context: PropsRequestContext): Promise<SandboxResults>;
5
+ export default function runCode(codes: string[], context: SsrRequestContext): Promise<SandboxResults>;
8
6
  export {};
9
7
  //# sourceMappingURL=sandbox.d.ts.map
@@ -1,41 +1,9 @@
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.getContext
6
- * function to provide prop request context object with properties such as props, params, and locale
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.getContext = () => 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', (err) => resolve({ error: `SSR worker exited with error: ${err.message}\n${err.stack}` }));
31
- worker.on('exit', (code) => {
32
- if (code !== 0) {
33
- resolve({ error: `SSR worker stopped with exit code: ${code}` });
34
- }
35
- });
36
- });
37
- }
1
+ import runCodeOnWorker from './sandbox-worker.js';
2
+ import runCodeOnLocker from './sandbox-locker.js';
38
3
  export default function runCode(codes, context) {
4
+ if (process.env.LOCKER_SB === 'true') {
5
+ return runCodeOnLocker(codes, context);
6
+ }
39
7
  return runCodeOnWorker(codes, context);
40
8
  }
41
9
  //# sourceMappingURL=sandbox.js.map
@@ -1,4 +1,7 @@
1
- import type { Json, ModuleBundler, ViewTranformPluginContext } from '@lwrjs/types';
1
+ import type { Json, ModuleBundler, PageDataResponse, ViewTranformPluginContext } from '@lwrjs/types';
2
+ interface SsrResults extends PageDataResponse {
3
+ html: string;
4
+ }
2
5
  /**
3
6
  * Create a bundle for the given SSR module and run the code in a sandbox.
4
7
  * @param moduleInfo - specifier: The ID of the module, generated by "lwc-ssr/moduleProvider", which SSRs a component
@@ -10,8 +13,6 @@ import type { Json, ModuleBundler, ViewTranformPluginContext } from '@lwrjs/type
10
13
  export declare function ssrElement({ specifier, props: templateProps }: {
11
14
  specifier: string;
12
15
  props: Json;
13
- }, moduleBundler: ModuleBundler, { runtimeEnvironment, runtimeParams }: ViewTranformPluginContext): Promise<{
14
- html: string;
15
- props: Json;
16
- }>;
16
+ }, moduleBundler: ModuleBundler, { runtimeEnvironment, runtimeParams }: ViewTranformPluginContext): Promise<SsrResults>;
17
+ export {};
17
18
  //# sourceMappingURL=ssr-element.d.ts.map
@@ -1,11 +1,6 @@
1
1
  import getCode from './amd-utils.js';
2
2
  import runCode from './sandbox.js';
3
- const bundleConfigOverrides = {
4
- exclude: [],
5
- alias: {
6
- lwc: '@lwc/engine-server', // override the default "@lwc/engine-dom" package
7
- },
8
- };
3
+ import { performance } from 'perf_hooks';
9
4
  /**
10
5
  * Create a bundle for the given SSR module and run the code in a sandbox.
11
6
  * @param moduleInfo - specifier: The ID of the module, generated by "lwc-ssr/moduleProvider", which SSRs a component
@@ -15,12 +10,23 @@ const bundleConfigOverrides = {
15
10
  * @returns a promise to the SSRed code string
16
11
  */
17
12
  export async function ssrElement({ specifier, props: templateProps }, moduleBundler, { runtimeEnvironment, runtimeParams }) {
18
- const { format, bundle } = runtimeEnvironment;
19
- const { bundleRecord, code, specifier: bundleSpecifier, version, } = await moduleBundler.getModuleBundle({ specifier },
20
- // Ensure the bundle flag is always off in ESM,
21
- // otherwise TOO much gets bundled in the module registry
22
- // in ESM, resulting lwc clashes/duplication
23
- { ...runtimeEnvironment, bundle: format === 'esm' ? false : bundle }, undefined, bundleConfigOverrides);
13
+ const { format } = runtimeEnvironment;
14
+ const { bundleRecord, code, specifier: bundleSpecifier, version, } = format === 'esm'
15
+ ? await moduleBundler.getModuleBundle({ specifier },
16
+ // Ensure the bundle flag is always off in ESM,
17
+ // otherwise TOO much gets bundled in the module registry
18
+ // in ESM, resulting lwc clashes/duplication
19
+ { ...runtimeEnvironment, bundle: false }, undefined, {
20
+ exclude: [],
21
+ alias: {
22
+ // override the default "@lwc/engine-dom" package
23
+ lwc: '@lwc/engine-server',
24
+ },
25
+ })
26
+ : await moduleBundler.getModuleBundle({ specifier }, runtimeEnvironment, undefined, {
27
+ // "lwc" will be defined as an alias to "@lwc/engine-server" in the AMD worker code.
28
+ exclude: ['lwc'],
29
+ });
24
30
  // Gather context to send into the SSR sandbox
25
31
  const context = {
26
32
  props: templateProps,
@@ -29,18 +35,16 @@ export async function ssrElement({ specifier, props: templateProps }, moduleBund
29
35
  locale: runtimeParams.locale || runtimeEnvironment.defaultLocale,
30
36
  };
31
37
  // Get the SSR string and properties bag
32
- const { error, result, props } = runtimeEnvironment.format === 'amd'
38
+ const startTime = performance.now();
39
+ const { result, props, markup } = format === 'amd'
33
40
  ? await runCode([
34
41
  ...(await getCode(runtimeEnvironment, version.replace(/\./g, '_'), bundleSpecifier, bundleRecord.includedModules)),
35
42
  code,
36
43
  ], context)
37
44
  : await runCode([code], context);
38
- if (error) {
39
- console.error(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, error);
40
- return { html: '', props: undefined };
41
- }
42
- else {
43
- return { html: result, props };
44
- }
45
+ const endTime = performance.now();
46
+ const timeDiff = endTime - startTime;
47
+ console.log(`[${specifier} SSR] complete in ${timeDiff} ms`);
48
+ return { html: result, props, markup };
45
49
  }
46
50
  //# 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.8.0-alpha.8",
7
+ "version": "0.9.0-alpha.0",
8
8
  "homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
9
9
  "repository": {
10
10
  "type": "git",
@@ -33,14 +33,16 @@
33
33
  "build/**/*.d.ts"
34
34
  ],
35
35
  "dependencies": {
36
- "@lwrjs/diagnostics": "0.8.0-alpha.8",
37
- "@lwrjs/shared-utils": "0.8.0-alpha.8"
36
+ "@locker/near-membrane-node": "^0.11.6",
37
+ "@lwrjs/diagnostics": "0.9.0-alpha.0",
38
+ "@lwrjs/shared-utils": "0.9.0-alpha.0",
39
+ "node-fetch": "^2.6.1"
38
40
  },
39
41
  "devDependencies": {
40
- "@lwrjs/types": "0.8.0-alpha.8"
42
+ "@lwrjs/types": "0.9.0-alpha.0"
41
43
  },
42
44
  "engines": {
43
45
  "node": ">=14.15.4 <19"
44
46
  },
45
- "gitHead": "345e7545d2d1d1f587567aacf0a072473c746d9e"
47
+ "gitHead": "6890d8619b295a49ee1ed8253a372337d83863be"
46
48
  }