@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 can be ["hydrated"](#client-hydration) to create an interactive web app.
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": "0.9.0"
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
- Data in `PageDataResponse.props` is serialized into the page document as JSON for each root component. LWR passes this data to the root component as [public properties](<(https://developer.salesforce.com/docs/component-library/documentation/en/lwc/reactivity_public)>) during both SSR and [client hydration](#client-hydration). In order to receive the properties, the root component must declare them using `@api` (see the `data` property in the [example](#data-example) below).
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
- 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.
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
- let rootCode = _rootCode;
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
- return (0, import_ssr_element.ssrElement)({specifier, props}, moduleBundler, resourceRegistry, viewContext).then(({
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: props2 = {},
65
+ props = {},
62
66
  markup: {links = []} = {links: []},
63
67
  cache: {ttl} = {}
64
68
  }) => {
65
69
  pageTtl = (0, import_shared_utils.shortestTtl)(ttl, pageTtl);
66
- const propsId = (0, import_identity.getPropsId)();
67
- ssrProps[propsId] = props2;
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}`, ` ${import_identity.SSR_PROPS_ATTR}="${propsId}"`, remain].join("");
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
- import_shared_utils.logger.warn(`Server-side rendering for "${specifier}" failed. Reason: `, err.stack);
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
- // 1a) Add the bundles imported into the root component bundle
100
- let rootCode = _rootCode;
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
- return ssrElement({ specifier, props }, moduleBundler, resourceRegistry, viewContext)
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
- const propsId = getPropsId();
58
- ssrProps[propsId] = props;
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}`, ` ${SSR_PROPS_ATTR}="${propsId}"`, remain].join('');
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
- // Create HTML <link> strings for each item in the links array
63
- const relStr = rel ? ` rel="${rel}"` : '', asStr = as ? ` as="${as}"` : '', fetchStr = fetchpriority ? ` fetchpriority="${fetchpriority}"` : '';
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
- logger.warn(`Server-side rendering for "${specifier}" failed. Reason: `, err.stack);
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.13",
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.13",
38
- "@lwrjs/shared-utils": "0.10.0-alpha.13",
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.13"
42
+ "@lwrjs/types": "0.10.0-alpha.14"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0 <20"
46
46
  },
47
- "gitHead": "f6d142d5a027554cb1685389e0b173734149683d"
47
+ "gitHead": "f80dc1c18719b77c183f339027313be11d69f9dc"
48
48
  }