@lwrjs/lwc-ssr 0.10.0-alpha.13 → 0.10.0-alpha.14
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
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- [SSR execution](#ssr-execution)
|
|
20
20
|
- [Portability](#portability)
|
|
21
21
|
- [Synchronous code](#synchronous-code)
|
|
22
|
+
- [Styling](#styling)
|
|
22
23
|
- [Debugging](#debugging)
|
|
23
24
|
- [Debug logging]()
|
|
24
25
|
- [Breakpoints](#breakpoints)
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
|
|
29
30
|
### What is SSR?
|
|
30
31
|
|
|
31
|
-
[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
|
|
32
|
+
[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 may be ["hydrated"](#client-hydration) to create an interactive web app.
|
|
32
33
|
|
|
33
34
|
See the following RFCs from LWC:
|
|
34
35
|
|
|
@@ -53,7 +54,7 @@ Add SSR capabilities to an LWR app by including `@lwrjs/lwc-ssr` in its _package
|
|
|
53
54
|
// my-app/package.json
|
|
54
55
|
{
|
|
55
56
|
"dependencies": {
|
|
56
|
-
"@lwrjs/lwc-ssr": "
|
|
57
|
+
"@lwrjs/lwc-ssr": "latest"
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
```
|
|
@@ -160,7 +161,9 @@ In LWR, the SSR process runs in a sandbox. This sandbox supports `globalThis.fet
|
|
|
160
161
|
|
|
161
162
|
#### JSON
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
LWR passes this data to the root component as [public properties](<(https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public)>) during SSR. In order to receive the properties, the root component must declare them using the `@api` decorator (see the `data` property in the [example](#data-example) below).
|
|
165
|
+
|
|
166
|
+
If a root component is [hydrated](#client-hydration), its `PageDataResponse.props` are serialized into the page document as JSON, then passed in during client hydration.
|
|
164
167
|
|
|
165
168
|
#### Markup
|
|
166
169
|
|
|
@@ -229,7 +232,15 @@ LWR will use the shortest TTL value from **all** sources to set the `max-age` of
|
|
|
229
232
|
|
|
230
233
|
### Client hydration
|
|
231
234
|
|
|
232
|
-
|
|
235
|
+
Root components can opt-in to hydrate their SSRed HTML in the browser. 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. To opt-in to hydration, a root component must have the `lwr:hydrate` directive:
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<!-- my-app/src/content/contact.html -->
|
|
239
|
+
<my-contact lwr:hydrate></my-contact>
|
|
240
|
+
<!-- hydrated -->
|
|
241
|
+
<my-about></my-about>
|
|
242
|
+
<!-- NOT hydrated -->
|
|
243
|
+
```
|
|
233
244
|
|
|
234
245
|
LWC will log a "hydration warning" if the SSRed component HTML does not match the output of its _first rendering cycle_ on the client. Hydration warnings should be prevented because they result in unsightly UI shifting. To avoid hydration warnings, ensure that template updates only occur due to user interaction, data fetching, or other _asynchronous_ actions.
|
|
235
246
|
|
|
@@ -320,6 +331,10 @@ The LWC SSR process run in a single synchronous pass. So any asynchronous code *
|
|
|
320
331
|
- [`async`/`await`](https://www.w3schools.com/js/js_async.asp)
|
|
321
332
|
- [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
|
|
322
333
|
|
|
334
|
+
### Styling
|
|
335
|
+
|
|
336
|
+
To render correctly, SSRed components must use either [native shadow](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_mixed_shadow) or [light DOM](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_light_dom). SSRed components which depend on [synthetic shadow](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_dom) may not be styled correctly on the client. If they opt-in to [hydration](#client-hydration), warnings will be thrown on the client.
|
|
337
|
+
|
|
323
338
|
## Debugging
|
|
324
339
|
|
|
325
340
|
### Debug logging
|
|
@@ -102,18 +102,20 @@ async function getBundle(specifier, moduleBundler, runtimeEnvironment) {
|
|
|
102
102
|
async function bundle(specifier, moduleBundler, runtimeEnvironment, bundleConfigOverrides) {
|
|
103
103
|
return await moduleBundler.getModuleBundle({specifier}, runtimeEnvironment, void 0, bundleConfigOverrides);
|
|
104
104
|
}
|
|
105
|
+
async function bundleImports(bundleCode, imports = [], visited, moduleBundler, runtimeEnvironment) {
|
|
106
|
+
for (const {specifier} of imports) {
|
|
107
|
+
if (!visited.has(specifier)) {
|
|
108
|
+
visited.add(specifier);
|
|
109
|
+
const {code, bundleRecord} = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
110
|
+
bundleCode = await bundleImports(code, bundleRecord.imports, visited, moduleBundler, runtimeEnvironment) + bundleCode;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return bundleCode;
|
|
114
|
+
}
|
|
105
115
|
async function buildBundle(ssrSpecifier, moduleBundler, runtimeEnvironment) {
|
|
106
116
|
const rootSpecifier = ssrSpecifier.replace(import_identity.LWC_SSR_PREFIX, "");
|
|
107
117
|
const {code: _rootCode, bundleRecord: rootBundleRecord} = await bundle(rootSpecifier, moduleBundler, runtimeEnvironment);
|
|
108
|
-
|
|
109
|
-
if (rootBundleRecord.imports) {
|
|
110
|
-
for (const {specifier} of rootBundleRecord.imports) {
|
|
111
|
-
if (specifier !== "lwc") {
|
|
112
|
-
const {code: code2} = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
113
|
-
rootCode = code2 + rootCode;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
118
|
+
const rootCode = await bundleImports(_rootCode, rootBundleRecord.imports, new Set(["lwc", rootSpecifier]), moduleBundler, runtimeEnvironment);
|
|
117
119
|
const {code: lwcEngineCode, version: lwcVersion} = await bundle(lwcEngineSpecifier, moduleBundler, runtimeEnvironment);
|
|
118
120
|
const {
|
|
119
121
|
bundleRecord,
|
|
@@ -39,11 +39,12 @@ function lwcSsrViewTransformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
39
39
|
import_shared_utils.logger.debug("[lwcSsrViewTransformer] link");
|
|
40
40
|
import_shared_utils.logger.verbose("[lwcSsrViewTransformer] link input", stringBuilder);
|
|
41
41
|
const ssrModules = [];
|
|
42
|
-
for (const {tagName, location, props} of customElements) {
|
|
42
|
+
for (const [index, {tagName, location, props}] of customElements.entries()) {
|
|
43
43
|
if (location) {
|
|
44
44
|
const {startOffset, endOffset} = location;
|
|
45
45
|
const moduleSpecifier = (0, import_shared_utils.kebabCaseToModuleSpecifier)(tagName);
|
|
46
46
|
ssrModules.push({
|
|
47
|
+
index,
|
|
47
48
|
startOffset,
|
|
48
49
|
endOffset,
|
|
49
50
|
props,
|
|
@@ -55,18 +56,25 @@ function lwcSsrViewTransformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
55
56
|
const ssrProps = {};
|
|
56
57
|
let ssrLinks = "";
|
|
57
58
|
let pageTtl;
|
|
58
|
-
await Promise.all(ssrModules.map(({specifier, tagName, props, startOffset, endOffset}) => {
|
|
59
|
-
|
|
59
|
+
await Promise.all(ssrModules.map(({index, specifier, tagName, props: rawProps = {}, startOffset, endOffset}) => {
|
|
60
|
+
const hydrate = (0, import_shared_utils.hasHydrateDirective)(rawProps);
|
|
61
|
+
const passProps = {...rawProps};
|
|
62
|
+
delete passProps[import_shared_utils.HYDRATE_DIRECTIVE];
|
|
63
|
+
return (0, import_ssr_element.ssrElement)({specifier, props: passProps}, moduleBundler, resourceRegistry, viewContext).then(({
|
|
60
64
|
html,
|
|
61
|
-
props
|
|
65
|
+
props = {},
|
|
62
66
|
markup: {links = []} = {links: []},
|
|
63
67
|
cache: {ttl} = {}
|
|
64
68
|
}) => {
|
|
65
69
|
pageTtl = (0, import_shared_utils.shortestTtl)(ttl, pageTtl);
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
let propsAttr = "";
|
|
71
|
+
if (hydrate) {
|
|
72
|
+
const propsId = (0, import_identity.getPropsId)();
|
|
73
|
+
propsAttr = ` ${import_identity.SSR_PROPS_ATTR}="${propsId}"`;
|
|
74
|
+
ssrProps[propsId] = props;
|
|
75
|
+
}
|
|
68
76
|
const [, remain] = html.split(`<${tagName}`);
|
|
69
|
-
html = [`<${tagName}`,
|
|
77
|
+
html = [`<${tagName}`, propsAttr, remain].join("");
|
|
70
78
|
links.forEach(({href, rel, as, fetchpriority}) => {
|
|
71
79
|
const relStr = rel ? ` rel="${rel}"` : "", asStr = as ? ` as="${as}"` : "", fetchStr = fetchpriority ? ` fetchpriority="${fetchpriority}"` : "";
|
|
72
80
|
ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>
|
|
@@ -74,7 +82,8 @@ function lwcSsrViewTransformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
74
82
|
});
|
|
75
83
|
stringBuilder.overwrite(startOffset, endOffset, html);
|
|
76
84
|
}).catch((err) => {
|
|
77
|
-
|
|
85
|
+
customElements[index].props === void 0 ? customElements[index].props = {[import_shared_utils.HYDRATE_DIRECTIVE]: ""} : customElements[index].props[import_shared_utils.HYDRATE_DIRECTIVE] = "";
|
|
86
|
+
import_shared_utils.logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
|
|
78
87
|
});
|
|
79
88
|
}));
|
|
80
89
|
if (Object.keys(ssrProps).length) {
|
|
@@ -88,26 +88,30 @@ moduleBundler, runtimeEnvironment) {
|
|
|
88
88
|
async function bundle(specifier, moduleBundler, runtimeEnvironment, bundleConfigOverrides) {
|
|
89
89
|
return await moduleBundler.getModuleBundle({ specifier }, runtimeEnvironment, undefined, bundleConfigOverrides);
|
|
90
90
|
}
|
|
91
|
+
// Recursively bundle the static imports of a root bundle into a single bundle
|
|
92
|
+
async function bundleImports(bundleCode, imports = [], visited, moduleBundler, runtimeEnvironment) {
|
|
93
|
+
for (const { specifier } of imports) {
|
|
94
|
+
if (!visited.has(specifier)) {
|
|
95
|
+
visited.add(specifier);
|
|
96
|
+
// eslint-disable-next-line no-await-in-loop
|
|
97
|
+
const { code, bundleRecord } = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
98
|
+
bundleCode =
|
|
99
|
+
// eslint-disable-next-line no-await-in-loop
|
|
100
|
+
(await bundleImports(code, bundleRecord.imports, visited, moduleBundler, runtimeEnvironment)) + bundleCode;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return bundleCode;
|
|
104
|
+
}
|
|
91
105
|
// Build a SSR bundle for a root component by concatenating:
|
|
92
106
|
// - the root component bundle
|
|
93
107
|
// - @lwc/engine-server
|
|
94
108
|
// - the SSR bundle for the root specifier
|
|
95
109
|
async function buildBundle(ssrSpecifier, moduleBundler, runtimeEnvironment) {
|
|
96
|
-
// 1. Get the bundle for the root component
|
|
110
|
+
// 1. Get the bundle for the root component, including all static dependencies
|
|
97
111
|
const rootSpecifier = ssrSpecifier.replace(LWC_SSR_PREFIX, '');
|
|
98
112
|
const { code: _rootCode, bundleRecord: rootBundleRecord } = await bundle(rootSpecifier, moduleBundler, runtimeEnvironment);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (rootBundleRecord.imports) {
|
|
102
|
-
for (const { specifier } of rootBundleRecord.imports) {
|
|
103
|
-
if (specifier !== 'lwc') {
|
|
104
|
-
// TODO: recurse the imports to support chains of excluded modules
|
|
105
|
-
// eslint-disable-next-line no-await-in-loop
|
|
106
|
-
const { code } = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
107
|
-
rootCode = code + rootCode;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
113
|
+
const rootCode = await bundleImports(_rootCode, rootBundleRecord.imports, new Set(['lwc', rootSpecifier]), // visited (lwc is excluded)
|
|
114
|
+
moduleBundler, runtimeEnvironment);
|
|
111
115
|
// 2. Get the bundle for the LWC engine
|
|
112
116
|
const { code: lwcEngineCode, version: lwcVersion } = await bundle(lwcEngineSpecifier, moduleBundler, runtimeEnvironment);
|
|
113
117
|
// 3. Get the SSR bundle for the root component
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { kebabCaseToModuleSpecifier, logger, shortestTtl } from '@lwrjs/shared-utils';
|
|
1
|
+
import { HYDRATE_DIRECTIVE, hasHydrateDirective, kebabCaseToModuleSpecifier, logger, shortestTtl, } from '@lwrjs/shared-utils';
|
|
2
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
|
/**
|
|
@@ -30,11 +30,12 @@ export default function lwcSsrViewTransformer(options, { moduleBundler, resource
|
|
|
30
30
|
logger.verbose('[lwcSsrViewTransformer] link input', stringBuilder);
|
|
31
31
|
// Gather all the SSRable custom elements (ie: root components) into 1 list
|
|
32
32
|
const ssrModules = [];
|
|
33
|
-
for (const { tagName, location, props } of customElements) {
|
|
33
|
+
for (const [index, { tagName, location, props }] of customElements.entries()) {
|
|
34
34
|
if (location) {
|
|
35
35
|
const { startOffset, endOffset } = location;
|
|
36
36
|
const moduleSpecifier = kebabCaseToModuleSpecifier(tagName);
|
|
37
37
|
ssrModules.push({
|
|
38
|
+
index,
|
|
38
39
|
startOffset,
|
|
39
40
|
endOffset,
|
|
40
41
|
props,
|
|
@@ -47,27 +48,44 @@ export default function lwcSsrViewTransformer(options, { moduleBundler, resource
|
|
|
47
48
|
const ssrProps = {};
|
|
48
49
|
let ssrLinks = '';
|
|
49
50
|
let pageTtl;
|
|
50
|
-
await Promise.all(ssrModules.map(({ specifier, tagName, props, startOffset, endOffset }) => {
|
|
51
|
-
|
|
51
|
+
await Promise.all(ssrModules.map(({ index, specifier, tagName, props: rawProps = {}, startOffset, endOffset }) => {
|
|
52
|
+
const hydrate = hasHydrateDirective(rawProps);
|
|
53
|
+
const passProps = { ...rawProps };
|
|
54
|
+
delete passProps[HYDRATE_DIRECTIVE];
|
|
55
|
+
return ssrElement({ specifier, props: passProps }, moduleBundler, resourceRegistry, viewContext)
|
|
52
56
|
.then(({ html, props = {}, markup: { links = [] } = { links: [] }, cache: { ttl } = {}, }) => {
|
|
53
57
|
// Keep track of the shortest TTL from all getPageData hooks
|
|
54
58
|
pageTtl = shortestTtl(ttl, pageTtl);
|
|
55
59
|
// Add the props id to the HTML for the custom element
|
|
56
60
|
// eg: <some-cmp> -> <some-cmp data-lwr-props-id="1234">
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
let propsAttr = '';
|
|
62
|
+
if (hydrate) {
|
|
63
|
+
// Only serialize props for custom elements that are to be hydrated
|
|
64
|
+
const propsId = getPropsId();
|
|
65
|
+
propsAttr = ` ${SSR_PROPS_ATTR}="${propsId}"`;
|
|
66
|
+
ssrProps[propsId] = props;
|
|
67
|
+
}
|
|
59
68
|
const [, remain] = html.split(`<${tagName}`);
|
|
60
|
-
html = [`<${tagName}`,
|
|
69
|
+
html = [`<${tagName}`, propsAttr, remain].join('');
|
|
70
|
+
// Create HTML <link> strings for each item in the links array
|
|
61
71
|
links.forEach(({ href, rel, as, fetchpriority }) => {
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
const relStr = rel ? ` rel="${rel}"` : '', asStr = as ? ` as="${as}"` : '', fetchStr = fetchpriority
|
|
73
|
+
? ` fetchpriority="${fetchpriority}"`
|
|
74
|
+
: '';
|
|
64
75
|
ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>\n`;
|
|
65
76
|
});
|
|
66
77
|
// Overwrite the custom element with the SSRed component string
|
|
67
78
|
stringBuilder.overwrite(startOffset, endOffset, html);
|
|
68
79
|
})
|
|
69
80
|
.catch((err) => {
|
|
70
|
-
|
|
81
|
+
// Fallback to CSR by adding the lwr:hydrate directive to the custom element
|
|
82
|
+
// This ENSURES the component's JavaScript gets sent to the client for CSRing
|
|
83
|
+
customElements[index].props === undefined
|
|
84
|
+
? (customElements[index].props = { [HYDRATE_DIRECTIVE]: '' })
|
|
85
|
+
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
86
|
+
// @ts-ignore - TS thinks that props may still be undefined
|
|
87
|
+
(customElements[index].props[HYDRATE_DIRECTIVE] = '');
|
|
88
|
+
logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
|
|
71
89
|
});
|
|
72
90
|
}));
|
|
73
91
|
if (Object.keys(ssrProps).length) {
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.10.0-alpha.
|
|
7
|
+
"version": "0.10.0-alpha.14",
|
|
8
8
|
"homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@locker/near-membrane-node": "^0.12.0",
|
|
37
|
-
"@lwrjs/diagnostics": "0.10.0-alpha.
|
|
38
|
-
"@lwrjs/shared-utils": "0.10.0-alpha.
|
|
37
|
+
"@lwrjs/diagnostics": "0.10.0-alpha.14",
|
|
38
|
+
"@lwrjs/shared-utils": "0.10.0-alpha.14",
|
|
39
39
|
"node-fetch": "^2.6.8"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@lwrjs/types": "0.10.0-alpha.
|
|
42
|
+
"@lwrjs/types": "0.10.0-alpha.14"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0 <20"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "f80dc1c18719b77c183f339027313be11d69f9dc"
|
|
48
48
|
}
|