@lwrjs/lwc-ssr 0.10.0-alpha.8 → 0.10.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 +73 -7
- package/build/cjs/moduleProvider/index.cjs +26 -3
- package/build/cjs/viewTransformer/amd-utils.cjs +39 -24
- package/build/cjs/viewTransformer/index.cjs +30 -14
- package/build/cjs/viewTransformer/sandbox-locker.cjs +7 -0
- package/build/cjs/viewTransformer/sandbox-worker.cjs +3 -1
- package/build/cjs/viewTransformer/ssr-element.cjs +20 -10
- package/build/es/moduleProvider/index.d.ts +1 -1
- package/build/es/moduleProvider/index.js +26 -3
- package/build/es/viewTransformer/amd-utils.js +56 -34
- package/build/es/viewTransformer/index.d.ts +1 -1
- package/build/es/viewTransformer/index.js +41 -15
- package/build/es/viewTransformer/sandbox-locker.js +7 -0
- package/build/es/viewTransformer/sandbox-worker.js +3 -0
- package/build/es/viewTransformer/sandbox.d.ts +1 -1
- package/build/es/viewTransformer/ssr-element.d.ts +2 -2
- package/build/es/viewTransformer/ssr-element.js +22 -12
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
- [Add the SSR package dependency](#add-the-ssr-package-dependency)
|
|
8
8
|
- [Turn on SSR](#turn-on-ssr)
|
|
9
9
|
- [Building SSR pages](#building-ssr-pages)
|
|
10
|
+
- [SSR detection](#ssr-detection)
|
|
10
11
|
- [Loading data during SSR](#loading-data-during-ssr)
|
|
11
12
|
- [Caching](#caching)
|
|
12
13
|
- [Client hydration](#client-hydration)
|
|
14
|
+
- [Skip SSR](#skip-ssr)
|
|
13
15
|
- [Limitations](#limitations)
|
|
14
16
|
- [Synthetic shadow](#synthetic-shadow)
|
|
15
17
|
- [Locker](#locker)
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
- [SSR execution](#ssr-execution)
|
|
19
21
|
- [Portability](#portability)
|
|
20
22
|
- [Synchronous code](#synchronous-code)
|
|
23
|
+
- [Styling](#styling)
|
|
21
24
|
- [Debugging](#debugging)
|
|
22
25
|
- [Debug logging]()
|
|
23
26
|
- [Breakpoints](#breakpoints)
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
|
|
28
31
|
### What is SSR?
|
|
29
32
|
|
|
30
|
-
[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
|
|
33
|
+
[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.
|
|
31
34
|
|
|
32
35
|
See the following RFCs from LWC:
|
|
33
36
|
|
|
@@ -52,7 +55,7 @@ Add SSR capabilities to an LWR app by including `@lwrjs/lwc-ssr` in its _package
|
|
|
52
55
|
// my-app/package.json
|
|
53
56
|
{
|
|
54
57
|
"dependencies": {
|
|
55
|
-
"@lwrjs/lwc-ssr": "
|
|
58
|
+
"@lwrjs/lwc-ssr": "latest"
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
```
|
|
@@ -98,6 +101,35 @@ LWR will automatically pass any root component attributes from a [template](http
|
|
|
98
101
|
</section>
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
### SSR detection
|
|
105
|
+
|
|
106
|
+
Check the `import.meta.env.SSR` flag to determine where component code is running. On the server it will be `true`, and on the client it will be [falsy](https://medium.com/coding-at-dawn/what-are-falsy-values-in-javascript-ca0faa34feb4). Component developers can use this flag to control in which environment code is run:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// pivot on the SSR flag
|
|
110
|
+
export default class App extends LightningElement {
|
|
111
|
+
connectedCallback() {
|
|
112
|
+
if (import.meta.env.SSR) {
|
|
113
|
+
// run code on the server
|
|
114
|
+
} else {
|
|
115
|
+
// run code on the client
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When using typescript, LWR augments the global `ImportMeta` interface in the `@lwrjs/types` package:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
declare global {
|
|
125
|
+
interface ImportMeta {
|
|
126
|
+
readonly env: {
|
|
127
|
+
SSR: boolean;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
101
133
|
### Loading data during SSR
|
|
102
134
|
|
|
103
135
|
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).
|
|
@@ -142,7 +174,9 @@ In LWR, the SSR process runs in a sandbox. This sandbox supports `globalThis.fet
|
|
|
142
174
|
|
|
143
175
|
#### JSON
|
|
144
176
|
|
|
145
|
-
|
|
177
|
+
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).
|
|
178
|
+
|
|
179
|
+
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.
|
|
146
180
|
|
|
147
181
|
#### Markup
|
|
148
182
|
|
|
@@ -211,7 +245,35 @@ LWR will use the shortest TTL value from **all** sources to set the `max-age` of
|
|
|
211
245
|
|
|
212
246
|
### Client hydration
|
|
213
247
|
|
|
214
|
-
|
|
248
|
+
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:
|
|
249
|
+
|
|
250
|
+
```html
|
|
251
|
+
<!-- my-app/src/content/contact.html -->
|
|
252
|
+
|
|
253
|
+
<!-- hydrated -->
|
|
254
|
+
<my-contact lwr:hydrate></my-contact>
|
|
255
|
+
|
|
256
|
+
<!-- NOT hydrated -->
|
|
257
|
+
<my-about></my-about>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
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.
|
|
261
|
+
|
|
262
|
+
### Skip SSR
|
|
263
|
+
|
|
264
|
+
Root components can skip SSR by setting the `lwr:hydrate` directive to `client-only`:
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<!-- my-app/src/content/location.html -->
|
|
268
|
+
|
|
269
|
+
<!-- SSRed -->
|
|
270
|
+
<my-info></my-info>
|
|
271
|
+
|
|
272
|
+
<!-- NOT SSRed -->
|
|
273
|
+
<my-map lwr:hydrate="client-only"></my-map>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Root components which opt-out of SSR will be fully rendered on the client using the [LWC `createElement()` API](https://www.npmjs.com/package/@lwc/engine-dom).
|
|
215
277
|
|
|
216
278
|
## Limitations
|
|
217
279
|
|
|
@@ -264,20 +326,20 @@ Examples of non-portable code and objects include:
|
|
|
264
326
|
- `window`
|
|
265
327
|
- `document`
|
|
266
328
|
- selector functions (`template.querySelector`, `template.querySelectorAll`, ...)
|
|
267
|
-
- `classList`
|
|
268
329
|
- JavaScript eventing
|
|
269
330
|
|
|
270
331
|
Isomorphic components can:
|
|
271
332
|
|
|
272
333
|
- place non-portable code in a function which is not [executed during SSR](#ssr-execution), such as the `renderedCallback`.
|
|
334
|
+
- import non-portable code [dynamically](#synchronous-code), so it does not get executed on the server.
|
|
273
335
|
- choose LWC APIs which are supported on both the client and server (eg: [`lwc:inner-html`](https://rfcs.lwc.dev/rfcs/lwc/0123-innerhtml-binding) over `appendChild()`).
|
|
274
|
-
- guard non-portable objects so they are not accessed during SSR:
|
|
336
|
+
- guard non-portable objects so they are not accessed during SSR using the [`import.meta.env.SSR` flag](#ssr-detection):
|
|
275
337
|
|
|
276
338
|
```js
|
|
277
339
|
// guard usage of the window object so it does not throw during SSR
|
|
278
340
|
export default class App extends LightningElement {
|
|
279
341
|
connectedCallback() {
|
|
280
|
-
if (
|
|
342
|
+
if (!import.meta.env.SSR) {
|
|
281
343
|
window.addEventListener('error', (evt) => {
|
|
282
344
|
console.error(`⚠️ Uncaught error: ${evt.message}`);
|
|
283
345
|
});
|
|
@@ -299,6 +361,10 @@ The LWC SSR process run in a single synchronous pass. So any asynchronous code *
|
|
|
299
361
|
- [`async`/`await`](https://www.w3schools.com/js/js_async.asp)
|
|
300
362
|
- [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
|
|
301
363
|
|
|
364
|
+
### Styling
|
|
365
|
+
|
|
366
|
+
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.
|
|
367
|
+
|
|
302
368
|
## Debugging
|
|
303
369
|
|
|
304
370
|
### Debug logging
|
|
@@ -35,25 +35,48 @@ import { renderComponent } from '@lwc/engine-server';
|
|
|
35
35
|
import Ctor, * as rootComponent from '${rootSpecifier}';
|
|
36
36
|
|
|
37
37
|
(async () => {
|
|
38
|
-
let result, props, markup;
|
|
38
|
+
let result, props, markup, existingTaskCount;
|
|
39
39
|
try {
|
|
40
40
|
// 1. setup page data
|
|
41
41
|
const context = globalThis.getContext();
|
|
42
42
|
props = context.props;
|
|
43
43
|
if (rootComponent.getPageData) {
|
|
44
|
-
const data = await
|
|
44
|
+
const data = await globalThis.trace({
|
|
45
|
+
name: 'lwr.view.getPageData',
|
|
46
|
+
attributes: {
|
|
47
|
+
specifier: '${rootSpecifier}'
|
|
48
|
+
}
|
|
49
|
+
}, () => rootComponent.getPageData(context));
|
|
50
|
+
|
|
45
51
|
props = data.props; // overwrite public props
|
|
46
52
|
markup = data.markup;
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
existingTaskCount = process.getActiveResourcesInfo
|
|
56
|
+
? process.getActiveResourcesInfo().length
|
|
57
|
+
: 0;
|
|
58
|
+
|
|
49
59
|
// 2. render component
|
|
50
|
-
result =
|
|
60
|
+
result = globalThis.trace({
|
|
61
|
+
name: 'lwr.view.renderComponent',
|
|
62
|
+
attributes: {
|
|
63
|
+
specifier: '${rootSpecifier}'
|
|
64
|
+
}
|
|
65
|
+
}, () => renderComponent('${(0, import_shared_utils.moduleSpecifierToKebabCase)(rootSpecifier)}', Ctor, props || {}));
|
|
51
66
|
|
|
52
67
|
} catch(e) {
|
|
53
68
|
// (3) relay error
|
|
54
69
|
globalThis.resolver({ error: e.message || e });
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
const currentTaskCount = process.getActiveResourcesInfo
|
|
73
|
+
? process.getActiveResourcesInfo().length
|
|
74
|
+
: 0;
|
|
75
|
+
|
|
76
|
+
if (currentTaskCount - existingTaskCount > 0) {
|
|
77
|
+
console.warn('[warn] async tasks encountered while server rendering "${rootSpecifier}"');
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
// 3. relay successful results
|
|
58
81
|
globalThis.resolver({ result, props, markup });
|
|
59
82
|
})()`;
|
|
@@ -29,7 +29,7 @@ __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
|
-
var
|
|
32
|
+
var LWC_SPECIFIERS = {csr: "lwc", ssr: "@lwc/engine-server"};
|
|
33
33
|
async function readableToString(readable) {
|
|
34
34
|
let result = "";
|
|
35
35
|
for await (const chunk of readable) {
|
|
@@ -70,27 +70,23 @@ function getLwrConfig(bundleSpecifier, lwrVersion) {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
73
|
+
function aliasLwcEngine(ssrSpecifier, aliases) {
|
|
74
|
+
const csrSpecifier = ssrSpecifier.replace(aliases.ssr, aliases.csr);
|
|
75
|
+
const ssrLwcAlias = (0, import_shared_utils.createAmdAlias)(csrSpecifier, ssrSpecifier);
|
|
76
|
+
const unversionedAlias = (0, import_shared_utils.createAmdAlias)(LWC_SPECIFIERS.csr, ssrSpecifier);
|
|
77
|
+
return `${ssrLwcAlias}${unversionedAlias}`;
|
|
75
78
|
}
|
|
76
79
|
var GLOBALTHIS_LWR = `globalThis.LWR = globalThis.LWR || {};`;
|
|
77
80
|
async function getCode(runtimeEnvironment, lwrVersion, bundleSpecifier, includedModules, resourceRegistry) {
|
|
78
81
|
const loaderShimSource2 = await getLoaderShim(resourceRegistry, runtimeEnvironment);
|
|
79
82
|
const lwrConfigString = JSON.stringify(getLwrConfig(bundleSpecifier, lwrVersion));
|
|
80
|
-
const lwcSpecifier = includedModules.
|
|
81
|
-
if (includedModule.startsWith("lwc/v")) {
|
|
82
|
-
return includedModule;
|
|
83
|
-
}
|
|
84
|
-
if (!specifier && includedModule.startsWith(`${lwcEngineSpecifier}/v`)) {
|
|
85
|
-
return includedModule.replace(lwcEngineSpecifier, "lwc");
|
|
86
|
-
}
|
|
87
|
-
return specifier;
|
|
88
|
-
}, "");
|
|
83
|
+
const lwcSpecifier = includedModules.find((m) => m.startsWith(`${LWC_SPECIFIERS.ssr}/v`));
|
|
89
84
|
return [
|
|
90
85
|
GLOBALTHIS_LWR,
|
|
91
86
|
`Object.assign(globalThis.LWR, ${lwrConfigString});`,
|
|
87
|
+
`Object.assign(globalThis.LWR, { onError: (err) => globalThis.resolver({ error: err.message })});`,
|
|
92
88
|
loaderShimSource2 ? loaderShimSource2 : "",
|
|
93
|
-
lwcSpecifier ?
|
|
89
|
+
lwcSpecifier ? aliasLwcEngine(lwcSpecifier, LWC_SPECIFIERS) : ""
|
|
94
90
|
];
|
|
95
91
|
}
|
|
96
92
|
async function getBundle(specifier, moduleBundler, runtimeEnvironment) {
|
|
@@ -102,29 +98,48 @@ async function getBundle(specifier, moduleBundler, runtimeEnvironment) {
|
|
|
102
98
|
async function bundle(specifier, moduleBundler, runtimeEnvironment, bundleConfigOverrides) {
|
|
103
99
|
return await moduleBundler.getModuleBundle({specifier}, runtimeEnvironment, void 0, bundleConfigOverrides);
|
|
104
100
|
}
|
|
101
|
+
async function bundleImports(bundleCode, imports = [], visited, moduleBundler, runtimeEnvironment) {
|
|
102
|
+
for (const {specifier} of imports) {
|
|
103
|
+
if (!visited.has(specifier)) {
|
|
104
|
+
visited.add(specifier);
|
|
105
|
+
const {code, bundleRecord, version} = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
106
|
+
let bundledCode;
|
|
107
|
+
if (runtimeEnvironment.featureFlags?.EXPERIMENTAL_UNVERSIONED_ALIASES) {
|
|
108
|
+
const versionedSpecifier = (0, import_shared_utils.getSpecifier)({specifier, version});
|
|
109
|
+
const aliasCode = (0, import_shared_utils.createAmdAlias)(versionedSpecifier, specifier);
|
|
110
|
+
bundledCode = [code, aliasCode].filter(Boolean).join("");
|
|
111
|
+
} else {
|
|
112
|
+
bundledCode = code;
|
|
113
|
+
}
|
|
114
|
+
bundleCode = await bundleImports(bundledCode, bundleRecord.imports, visited, moduleBundler, runtimeEnvironment) + bundleCode;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return bundleCode;
|
|
118
|
+
}
|
|
105
119
|
async function buildBundle(ssrSpecifier, moduleBundler, runtimeEnvironment) {
|
|
106
120
|
const rootSpecifier = ssrSpecifier.replace(import_identity.LWC_SSR_PREFIX, "");
|
|
107
121
|
const {code: _rootCode, bundleRecord: rootBundleRecord} = await bundle(rootSpecifier, moduleBundler, runtimeEnvironment);
|
|
108
|
-
let rootCode = _rootCode;
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
let rootCode = await bundleImports(_rootCode, rootBundleRecord.imports, new Set(["lwc", rootSpecifier]), moduleBundler, runtimeEnvironment);
|
|
123
|
+
if (runtimeEnvironment.featureFlags?.EXPERIMENTAL_UNVERSIONED_ALIASES) {
|
|
124
|
+
const aliasSpecifier = (0, import_shared_utils.getSpecifier)({
|
|
125
|
+
specifier: rootSpecifier,
|
|
126
|
+
version: "version-not-provided"
|
|
127
|
+
});
|
|
128
|
+
const aliasCode = (0, import_shared_utils.createAmdAlias)(aliasSpecifier, rootSpecifier);
|
|
129
|
+
rootCode = [aliasCode, rootCode].join("");
|
|
116
130
|
}
|
|
117
|
-
const {code: lwcEngineCode, version: lwcVersion} = await bundle(
|
|
131
|
+
const {code: lwcEngineCode, version: lwcVersion} = await bundle(LWC_SPECIFIERS.ssr, moduleBundler, runtimeEnvironment);
|
|
118
132
|
const {
|
|
119
133
|
bundleRecord,
|
|
120
134
|
code: ssrCode,
|
|
121
135
|
version: lwrVersion
|
|
122
136
|
} = await bundle(ssrSpecifier, moduleBundler, runtimeEnvironment, {
|
|
123
137
|
appendExcludes: true,
|
|
124
|
-
exclude: [
|
|
138
|
+
exclude: [LWC_SPECIFIERS.ssr, rootSpecifier]
|
|
125
139
|
});
|
|
126
140
|
const code = rootCode + lwcEngineCode + ssrCode;
|
|
127
|
-
bundleRecord.includedModules.push(
|
|
141
|
+
bundleRecord.includedModules.push(...rootBundleRecord.includedModules);
|
|
142
|
+
bundleRecord.includedModules.push(`${LWC_SPECIFIERS.ssr}/v/${(0, import_shared_utils.normalizeVersionToUri)(lwcVersion)}`);
|
|
128
143
|
return {
|
|
129
144
|
bundleRecord,
|
|
130
145
|
code,
|
|
@@ -24,26 +24,32 @@ var __toModule = (module2) => {
|
|
|
24
24
|
// packages/@lwrjs/lwc-ssr/src/viewTransformer/index.ts
|
|
25
25
|
__markAsModule(exports);
|
|
26
26
|
__export(exports, {
|
|
27
|
-
default: () =>
|
|
27
|
+
default: () => lwcSsrViewTransformer
|
|
28
28
|
});
|
|
29
29
|
var import_shared_utils = __toModule(require("@lwrjs/shared-utils"));
|
|
30
30
|
var import_identity = __toModule(require("../identity.cjs"));
|
|
31
31
|
var import_ssr_element = __toModule(require("./ssr-element.cjs"));
|
|
32
|
-
function
|
|
32
|
+
function lwcSsrViewTransformer(options, {moduleBundler, resourceRegistry}) {
|
|
33
33
|
return {
|
|
34
34
|
name: "ssr-lwc-transformer",
|
|
35
35
|
async link(stringBuilder, viewContext, {customElements}) {
|
|
36
36
|
if (!viewContext.view.bootstrap?.ssr) {
|
|
37
37
|
return {};
|
|
38
38
|
}
|
|
39
|
-
import_shared_utils.logger.debug("[
|
|
40
|
-
import_shared_utils.logger.verbose("[
|
|
39
|
+
import_shared_utils.logger.debug("[lwcSsrViewTransformer] link");
|
|
40
|
+
import_shared_utils.logger.verbose("[lwcSsrViewTransformer] link input", stringBuilder);
|
|
41
41
|
const ssrModules = [];
|
|
42
|
-
for (const {tagName, location, props} of customElements) {
|
|
43
|
-
|
|
42
|
+
for (const [index, {tagName, location, props}] of customElements.entries()) {
|
|
43
|
+
const isCsr = (0, import_shared_utils.isCsrIsland)(props);
|
|
44
|
+
if (isCsr && location) {
|
|
45
|
+
const {startOffset, endOffset} = location;
|
|
46
|
+
stringBuilder.overwrite(startOffset, endOffset, `<${tagName}></${tagName}>`);
|
|
47
|
+
}
|
|
48
|
+
if (!isCsr && location) {
|
|
44
49
|
const {startOffset, endOffset} = location;
|
|
45
50
|
const moduleSpecifier = (0, import_shared_utils.kebabCaseToModuleSpecifier)(tagName);
|
|
46
51
|
ssrModules.push({
|
|
52
|
+
index,
|
|
47
53
|
startOffset,
|
|
48
54
|
endOffset,
|
|
49
55
|
props,
|
|
@@ -55,18 +61,25 @@ function lwcSsrViewTranformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
55
61
|
const ssrProps = {};
|
|
56
62
|
let ssrLinks = "";
|
|
57
63
|
let pageTtl;
|
|
58
|
-
await Promise.all(ssrModules.map(({specifier, tagName, props, startOffset, endOffset}) => {
|
|
59
|
-
|
|
64
|
+
await Promise.all(ssrModules.map(({index, specifier, tagName, props: rawProps = {}, startOffset, endOffset}) => {
|
|
65
|
+
const hydrate = (0, import_shared_utils.isHydrateOnLoad)(rawProps);
|
|
66
|
+
const passProps = {...rawProps};
|
|
67
|
+
delete passProps[import_shared_utils.HYDRATE_DIRECTIVE];
|
|
68
|
+
return (0, import_ssr_element.ssrElement)({specifier, props: passProps}, moduleBundler, resourceRegistry, viewContext).then(({
|
|
60
69
|
html,
|
|
61
|
-
props
|
|
70
|
+
props = {},
|
|
62
71
|
markup: {links = []} = {links: []},
|
|
63
72
|
cache: {ttl} = {}
|
|
64
73
|
}) => {
|
|
65
74
|
pageTtl = (0, import_shared_utils.shortestTtl)(ttl, pageTtl);
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
let propsAttr = "";
|
|
76
|
+
if (hydrate) {
|
|
77
|
+
const propsId = (0, import_identity.getPropsId)();
|
|
78
|
+
propsAttr = ` ${import_identity.SSR_PROPS_ATTR}="${propsId}"`;
|
|
79
|
+
ssrProps[propsId] = props;
|
|
80
|
+
}
|
|
68
81
|
const [, remain] = html.split(`<${tagName}`);
|
|
69
|
-
html = [`<${tagName}`,
|
|
82
|
+
html = [`<${tagName}`, propsAttr, remain].join("");
|
|
70
83
|
links.forEach(({href, rel, as, fetchpriority}) => {
|
|
71
84
|
const relStr = rel ? ` rel="${rel}"` : "", asStr = as ? ` as="${as}"` : "", fetchStr = fetchpriority ? ` fetchpriority="${fetchpriority}"` : "";
|
|
72
85
|
ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>
|
|
@@ -74,7 +87,10 @@ function lwcSsrViewTranformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
74
87
|
});
|
|
75
88
|
stringBuilder.overwrite(startOffset, endOffset, html);
|
|
76
89
|
}).catch((err) => {
|
|
77
|
-
|
|
90
|
+
customElements[index].props === void 0 ? customElements[index].props = {
|
|
91
|
+
[import_shared_utils.HYDRATE_DIRECTIVE]: import_shared_utils.HYDRATE_CLIENT_VALUE
|
|
92
|
+
} : customElements[index].props[import_shared_utils.HYDRATE_DIRECTIVE] = import_shared_utils.HYDRATE_CLIENT_VALUE;
|
|
93
|
+
import_shared_utils.logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
|
|
78
94
|
});
|
|
79
95
|
}));
|
|
80
96
|
if (Object.keys(ssrProps).length) {
|
|
@@ -88,7 +104,7 @@ function lwcSsrViewTranformer(options, {moduleBundler, resourceRegistry}) {
|
|
|
88
104
|
import_shared_utils.logger.error("Adding links during server-side rendering failed. Could not find the </head> tag.");
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
|
-
import_shared_utils.logger.verbose("
|
|
107
|
+
import_shared_utils.logger.verbose("lwcSsrViewTransformer response", stringBuilder);
|
|
92
108
|
return {cache: {ttl: pageTtl}};
|
|
93
109
|
}
|
|
94
110
|
};
|
|
@@ -26,8 +26,10 @@ __markAsModule(exports);
|
|
|
26
26
|
__export(exports, {
|
|
27
27
|
default: () => runCode
|
|
28
28
|
});
|
|
29
|
+
var import_instrumentation = __toModule(require("@lwrjs/instrumentation"));
|
|
29
30
|
var import_near_membrane_node = __toModule(require("@locker/near-membrane-node"));
|
|
30
31
|
var import_node_fetch = __toModule(require("node-fetch"));
|
|
32
|
+
var import_crypto = __toModule(require("crypto"));
|
|
31
33
|
function runCode(codes, context) {
|
|
32
34
|
return new Promise((resolve, reject) => {
|
|
33
35
|
let resolver;
|
|
@@ -35,8 +37,13 @@ function runCode(codes, context) {
|
|
|
35
37
|
function getContext() {
|
|
36
38
|
return context;
|
|
37
39
|
}
|
|
40
|
+
const tracer = (0, import_instrumentation.getTracer)();
|
|
41
|
+
const trace = tracer.trace.bind(tracer);
|
|
38
42
|
const endowments = Object.getOwnPropertyDescriptors({
|
|
43
|
+
AbortController,
|
|
44
|
+
crypto: import_crypto.webcrypto,
|
|
39
45
|
getContext,
|
|
46
|
+
trace,
|
|
40
47
|
fetch: import_node_fetch.default,
|
|
41
48
|
resolver,
|
|
42
49
|
process,
|
|
@@ -32,7 +32,9 @@ var WORKER_CODE_SANDBOX_APIS = [
|
|
|
32
32
|
`const { parentPort, workerData } = require('worker_threads');`,
|
|
33
33
|
`globalThis.getContext = () => workerData;`,
|
|
34
34
|
`globalThis.fetch = require('node-fetch');`,
|
|
35
|
-
`globalThis.
|
|
35
|
+
`globalThis.crypto = require('crypto').webcrypto;`,
|
|
36
|
+
`globalThis.resolver = (...args) => parentPort.postMessage(...args);`,
|
|
37
|
+
`globalThis.trace = (id, fn) => fn()`
|
|
36
38
|
];
|
|
37
39
|
function runCode(codes, workerData) {
|
|
38
40
|
const workerCode = [HEADER, ...WORKER_CODE_SANDBOX_APIS, ...codes].join("\n");
|
|
@@ -30,6 +30,8 @@ var import_amd_utils = __toModule(require("./amd-utils.cjs"));
|
|
|
30
30
|
var import_sandbox = __toModule(require("./sandbox.cjs"));
|
|
31
31
|
var import_perf_hooks = __toModule(require("perf_hooks"));
|
|
32
32
|
var import_shared_utils = __toModule(require("@lwrjs/shared-utils"));
|
|
33
|
+
var import_instrumentation = __toModule(require("@lwrjs/instrumentation"));
|
|
34
|
+
var SSR_FLAG = "process.env.SSR = true;";
|
|
33
35
|
async function ssrElement({specifier, props: templateProps}, moduleBundler, resourceRegistry, {runtimeEnvironment, runtimeParams}) {
|
|
34
36
|
const {format} = runtimeEnvironment;
|
|
35
37
|
const {
|
|
@@ -50,14 +52,22 @@ async function ssrElement({specifier, props: templateProps}, moduleBundler, reso
|
|
|
50
52
|
locale: runtimeParams.locale || runtimeEnvironment.defaultLocale
|
|
51
53
|
};
|
|
52
54
|
const startTime = import_perf_hooks.performance.now();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
return (0, import_instrumentation.getTracer)().trace({
|
|
56
|
+
name: import_instrumentation.ViewSpan.ServerSideRender,
|
|
57
|
+
attributes: {
|
|
58
|
+
specifier: bundleSpecifier
|
|
59
|
+
}
|
|
60
|
+
}, async () => {
|
|
61
|
+
const {result, props, markup, cache, error} = format === "amd" ? await (0, import_sandbox.default)([
|
|
62
|
+
SSR_FLAG,
|
|
63
|
+
...await (0, import_amd_utils.getCode)(runtimeEnvironment, version.replace(/\./g, "_"), bundleSpecifier, bundleRecord.includedModules, resourceRegistry),
|
|
64
|
+
code
|
|
65
|
+
], context) : await (0, import_sandbox.default)([SSR_FLAG, code], context);
|
|
66
|
+
const endTime = import_perf_hooks.performance.now();
|
|
67
|
+
const timeDiff = endTime - startTime;
|
|
68
|
+
import_shared_utils.logger.info(`[Server-side Rendering] ${specifier} in ${Math.round(timeDiff)} ms`);
|
|
69
|
+
if (error)
|
|
70
|
+
throw new Error(error);
|
|
71
|
+
return {html: result, props, markup, cache};
|
|
72
|
+
});
|
|
63
73
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModuleCompiled, ModuleEntry, ModuleProvider, ProviderContext, AbstractModuleId } from '@lwrjs/types';
|
|
1
|
+
import type { ModuleCompiled, ModuleEntry, ModuleProvider, ProviderContext, AbstractModuleId } from '@lwrjs/types';
|
|
2
2
|
/**
|
|
3
3
|
* This module provider generates code which server-side renders a given component.
|
|
4
4
|
* It handles module specifiers in the form: "@lwrjs/lwc-ssr/${component specifier}".
|
|
@@ -19,25 +19,48 @@ import { renderComponent } from '@lwc/engine-server';
|
|
|
19
19
|
import Ctor, * as rootComponent from '${rootSpecifier}';
|
|
20
20
|
|
|
21
21
|
(async () => {
|
|
22
|
-
let result, props, markup;
|
|
22
|
+
let result, props, markup, existingTaskCount;
|
|
23
23
|
try {
|
|
24
24
|
// 1. setup page data
|
|
25
25
|
const context = globalThis.getContext();
|
|
26
26
|
props = context.props;
|
|
27
27
|
if (rootComponent.getPageData) {
|
|
28
|
-
const data = await
|
|
28
|
+
const data = await globalThis.trace({
|
|
29
|
+
name: 'lwr.view.getPageData',
|
|
30
|
+
attributes: {
|
|
31
|
+
specifier: '${rootSpecifier}'
|
|
32
|
+
}
|
|
33
|
+
}, () => rootComponent.getPageData(context));
|
|
34
|
+
|
|
29
35
|
props = data.props; // overwrite public props
|
|
30
36
|
markup = data.markup;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
existingTaskCount = process.getActiveResourcesInfo
|
|
40
|
+
? process.getActiveResourcesInfo().length
|
|
41
|
+
: 0;
|
|
42
|
+
|
|
33
43
|
// 2. render component
|
|
34
|
-
result =
|
|
44
|
+
result = globalThis.trace({
|
|
45
|
+
name: 'lwr.view.renderComponent',
|
|
46
|
+
attributes: {
|
|
47
|
+
specifier: '${rootSpecifier}'
|
|
48
|
+
}
|
|
49
|
+
}, () => renderComponent('${moduleSpecifierToKebabCase(rootSpecifier)}', Ctor, props || {}));
|
|
35
50
|
|
|
36
51
|
} catch(e) {
|
|
37
52
|
// (3) relay error
|
|
38
53
|
globalThis.resolver({ error: e.message || e });
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
const currentTaskCount = process.getActiveResourcesInfo
|
|
57
|
+
? process.getActiveResourcesInfo().length
|
|
58
|
+
: 0;
|
|
59
|
+
|
|
60
|
+
if (currentTaskCount - existingTaskCount > 0) {
|
|
61
|
+
console.warn('[warn] async tasks encountered while server rendering "${rootSpecifier}"');
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
// 3. relay successful results
|
|
42
65
|
globalThis.resolver({ result, props, markup });
|
|
43
66
|
})()`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getFeatureFlags, normalizeVersionToUri } from '@lwrjs/shared-utils';
|
|
1
|
+
import { createAmdAlias, getFeatureFlags, getSpecifier, normalizeVersionToUri } from '@lwrjs/shared-utils';
|
|
2
2
|
import { LWC_SSR_PREFIX } from '../identity.js';
|
|
3
|
-
const
|
|
3
|
+
const LWC_SPECIFIERS = { csr: 'lwc', ssr: '@lwc/engine-server' };
|
|
4
4
|
async function readableToString(readable) {
|
|
5
5
|
let result = '';
|
|
6
6
|
for await (const chunk of readable) {
|
|
@@ -46,33 +46,30 @@ function getLwrConfig(bundleSpecifier, lwrVersion) {
|
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Alias a CSR module specifier to its SSR implementation
|
|
50
|
+
// eg: lwc/v/x_y_z -> @lwc/engine-server/v/x_y_z
|
|
51
|
+
function aliasLwcEngine(ssrSpecifier, aliases) {
|
|
52
|
+
const csrSpecifier = ssrSpecifier.replace(aliases.ssr, aliases.csr);
|
|
53
|
+
const ssrLwcAlias = createAmdAlias(csrSpecifier, ssrSpecifier);
|
|
54
|
+
// TODO: remove pending TD-0148452
|
|
55
|
+
const unversionedAlias = createAmdAlias(LWC_SPECIFIERS.csr, ssrSpecifier);
|
|
56
|
+
return `${ssrLwcAlias}${unversionedAlias}`;
|
|
52
57
|
}
|
|
53
58
|
const GLOBALTHIS_LWR = `globalThis.LWR = globalThis.LWR || {};`;
|
|
54
59
|
export async function getCode(runtimeEnvironment, lwrVersion, bundleSpecifier, includedModules, resourceRegistry) {
|
|
55
60
|
const loaderShimSource = await getLoaderShim(resourceRegistry, runtimeEnvironment);
|
|
56
61
|
const lwrConfigString = JSON.stringify(getLwrConfig(bundleSpecifier, lwrVersion));
|
|
57
|
-
const lwcSpecifier = includedModules.
|
|
58
|
-
if (includedModule.startsWith('lwc/v')) {
|
|
59
|
-
return includedModule;
|
|
60
|
-
}
|
|
61
|
-
// define `lwc` when `@lwc/engine-server` is included in the bundle
|
|
62
|
-
// `lwc` will be excluded by default when bundling is enabled
|
|
63
|
-
if (!specifier && includedModule.startsWith(`${lwcEngineSpecifier}/v`)) {
|
|
64
|
-
return includedModule.replace(lwcEngineSpecifier, 'lwc');
|
|
65
|
-
}
|
|
66
|
-
return specifier;
|
|
67
|
-
}, '');
|
|
62
|
+
const lwcSpecifier = includedModules.find((m) => m.startsWith(`${LWC_SPECIFIERS.ssr}/v`));
|
|
68
63
|
// Order matters:
|
|
69
64
|
// 1. "globalThis.LWR" must be defined prior to executing the shim and loader
|
|
70
65
|
// 2. the lwc module override needs to be defined before lwc is [re]defined in the custom element code (first define wins)
|
|
71
66
|
return [
|
|
72
67
|
GLOBALTHIS_LWR,
|
|
73
68
|
`Object.assign(globalThis.LWR, ${lwrConfigString});`,
|
|
69
|
+
// attaching a custom error handler to catch async bootstrap errors
|
|
70
|
+
`Object.assign(globalThis.LWR, { onError: (err) => globalThis.resolver({ error: err.message })});`,
|
|
74
71
|
loaderShimSource ? loaderShimSource : '',
|
|
75
|
-
lwcSpecifier ?
|
|
72
|
+
lwcSpecifier ? aliasLwcEngine(lwcSpecifier, LWC_SPECIFIERS) : '',
|
|
76
73
|
];
|
|
77
74
|
}
|
|
78
75
|
export async function getBundle(specifier, // e.g. "@lwrjs/lwc-ssr/root/component"
|
|
@@ -88,37 +85,62 @@ moduleBundler, runtimeEnvironment) {
|
|
|
88
85
|
async function bundle(specifier, moduleBundler, runtimeEnvironment, bundleConfigOverrides) {
|
|
89
86
|
return await moduleBundler.getModuleBundle({ specifier }, runtimeEnvironment, undefined, bundleConfigOverrides);
|
|
90
87
|
}
|
|
88
|
+
// Recursively bundle the static imports of a root bundle into a single bundle
|
|
89
|
+
async function bundleImports(bundleCode, imports = [], visited, moduleBundler, runtimeEnvironment) {
|
|
90
|
+
for (const { specifier } of imports) {
|
|
91
|
+
if (!visited.has(specifier)) {
|
|
92
|
+
visited.add(specifier);
|
|
93
|
+
// eslint-disable-next-line no-await-in-loop
|
|
94
|
+
const { code, bundleRecord, version } = await bundle(specifier, moduleBundler, runtimeEnvironment);
|
|
95
|
+
let bundledCode;
|
|
96
|
+
if (runtimeEnvironment.featureFlags?.EXPERIMENTAL_UNVERSIONED_ALIASES) {
|
|
97
|
+
const versionedSpecifier = getSpecifier({ specifier, version });
|
|
98
|
+
const aliasCode = createAmdAlias(versionedSpecifier, specifier);
|
|
99
|
+
bundledCode = [code, aliasCode].filter(Boolean).join('');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
bundledCode = code;
|
|
103
|
+
}
|
|
104
|
+
bundleCode =
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
(await bundleImports(bundledCode, bundleRecord.imports, visited, moduleBundler, runtimeEnvironment)) + bundleCode;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return bundleCode;
|
|
110
|
+
}
|
|
91
111
|
// Build a SSR bundle for a root component by concatenating:
|
|
92
|
-
// - the root component bundle
|
|
112
|
+
// - the root component bundle and static imports
|
|
93
113
|
// - @lwc/engine-server
|
|
94
114
|
// - the SSR bundle for the root specifier
|
|
95
115
|
async function buildBundle(ssrSpecifier, moduleBundler, runtimeEnvironment) {
|
|
96
|
-
// 1. Get the bundle for the root component
|
|
116
|
+
// 1. Get the bundle for the root component, including all static dependencies
|
|
97
117
|
const rootSpecifier = ssrSpecifier.replace(LWC_SSR_PREFIX, '');
|
|
98
118
|
const { code: _rootCode, bundleRecord: rootBundleRecord } = await bundle(rootSpecifier, moduleBundler, runtimeEnvironment);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
let rootCode = await bundleImports(_rootCode, rootBundleRecord.imports, new Set(['lwc', rootSpecifier]), // visited (lwc is excluded)
|
|
120
|
+
moduleBundler, runtimeEnvironment);
|
|
121
|
+
// TODO: remove pending TD-0148452
|
|
122
|
+
if (runtimeEnvironment.featureFlags?.EXPERIMENTAL_UNVERSIONED_ALIASES) {
|
|
123
|
+
// Create an AMD alias to the unversioned root module
|
|
124
|
+
// if the bundle-metadata for the module doesn't have a version, it will have /v/version-not-provided`
|
|
125
|
+
const aliasSpecifier = getSpecifier({
|
|
126
|
+
specifier: rootSpecifier,
|
|
127
|
+
version: 'version-not-provided',
|
|
128
|
+
});
|
|
129
|
+
const aliasCode = createAmdAlias(aliasSpecifier, rootSpecifier);
|
|
130
|
+
rootCode = [aliasCode, rootCode].join('');
|
|
110
131
|
}
|
|
111
132
|
// 2. Get the bundle for the LWC engine
|
|
112
|
-
const { code: lwcEngineCode, version: lwcVersion } = await bundle(
|
|
113
|
-
// 3. Get the
|
|
133
|
+
const { code: lwcEngineCode, version: lwcVersion } = await bundle(LWC_SPECIFIERS.ssr, moduleBundler, runtimeEnvironment);
|
|
134
|
+
// 3. Get the bundle for the SSR root component
|
|
114
135
|
// Exclude the LWC engine and root component from this bundle. Use the pre-built bundles fetched above instead.
|
|
115
136
|
const { bundleRecord, code: ssrCode, version: lwrVersion, } = await bundle(ssrSpecifier, moduleBundler, runtimeEnvironment, {
|
|
116
137
|
appendExcludes: true,
|
|
117
|
-
exclude: [
|
|
138
|
+
exclude: [LWC_SPECIFIERS.ssr, rootSpecifier],
|
|
118
139
|
});
|
|
119
140
|
// Now concatenate the code gathered above to create the SSR bundle for the root component
|
|
120
141
|
const code = rootCode + lwcEngineCode + ssrCode;
|
|
121
|
-
bundleRecord.includedModules.push(
|
|
142
|
+
bundleRecord.includedModules.push(...rootBundleRecord.includedModules); // add the root component's inclusions
|
|
143
|
+
bundleRecord.includedModules.push(`${LWC_SPECIFIERS.ssr}/v/${normalizeVersionToUri(lwcVersion)}`); // for LWC aliasing
|
|
122
144
|
return {
|
|
123
145
|
bundleRecord,
|
|
124
146
|
code,
|
|
@@ -20,6 +20,6 @@ interface SsrPluginOptions {
|
|
|
20
20
|
* 4. The view/page document now contains SSRed components, which will be sent to the client
|
|
21
21
|
* 5. During bootstrap on the client, the "lwr/init" module will hydrate ALL the custom elements on the page
|
|
22
22
|
*/
|
|
23
|
-
export default function
|
|
23
|
+
export default function lwcSsrViewTransformer(options: SsrPluginOptions, { moduleBundler, resourceRegistry }: ProviderContext): ViewTransformPlugin;
|
|
24
24
|
export {};
|
|
25
25
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { kebabCaseToModuleSpecifier, logger, shortestTtl } from '@lwrjs/shared-utils';
|
|
1
|
+
import { HYDRATE_CLIENT_VALUE, HYDRATE_DIRECTIVE, isCsrIsland, isHydrateOnLoad, 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
|
/**
|
|
@@ -19,22 +19,29 @@ import { ssrElement } from './ssr-element.js';
|
|
|
19
19
|
* 4. The view/page document now contains SSRed components, which will be sent to the client
|
|
20
20
|
* 5. During bootstrap on the client, the "lwr/init" module will hydrate ALL the custom elements on the page
|
|
21
21
|
*/
|
|
22
|
-
export default function
|
|
22
|
+
export default function lwcSsrViewTransformer(options, { moduleBundler, resourceRegistry }) {
|
|
23
23
|
return {
|
|
24
24
|
name: 'ssr-lwc-transformer',
|
|
25
25
|
async link(stringBuilder, viewContext, { customElements }) {
|
|
26
26
|
if (!viewContext.view.bootstrap?.ssr) {
|
|
27
27
|
return {}; // no SSR
|
|
28
28
|
}
|
|
29
|
-
logger.debug('[
|
|
30
|
-
logger.verbose('[
|
|
29
|
+
logger.debug('[lwcSsrViewTransformer] link');
|
|
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) {
|
|
34
|
-
|
|
33
|
+
for (const [index, { tagName, location, props }] of customElements.entries()) {
|
|
34
|
+
const isCsr = isCsrIsland(props);
|
|
35
|
+
if (isCsr && location) {
|
|
36
|
+
// Strip the CSR-only island directive from the HTML
|
|
37
|
+
const { startOffset, endOffset } = location;
|
|
38
|
+
stringBuilder.overwrite(startOffset, endOffset, `<${tagName}></${tagName}>`);
|
|
39
|
+
}
|
|
40
|
+
if (!isCsr && location) {
|
|
35
41
|
const { startOffset, endOffset } = location;
|
|
36
42
|
const moduleSpecifier = kebabCaseToModuleSpecifier(tagName);
|
|
37
43
|
ssrModules.push({
|
|
44
|
+
index,
|
|
38
45
|
startOffset,
|
|
39
46
|
endOffset,
|
|
40
47
|
props,
|
|
@@ -47,27 +54,46 @@ export default function lwcSsrViewTranformer(options, { moduleBundler, resourceR
|
|
|
47
54
|
const ssrProps = {};
|
|
48
55
|
let ssrLinks = '';
|
|
49
56
|
let pageTtl;
|
|
50
|
-
await Promise.all(ssrModules.map(({ specifier, tagName, props, startOffset, endOffset }) => {
|
|
51
|
-
|
|
57
|
+
await Promise.all(ssrModules.map(({ index, specifier, tagName, props: rawProps = {}, startOffset, endOffset }) => {
|
|
58
|
+
const hydrate = isHydrateOnLoad(rawProps);
|
|
59
|
+
const passProps = { ...rawProps };
|
|
60
|
+
delete passProps[HYDRATE_DIRECTIVE];
|
|
61
|
+
return ssrElement({ specifier, props: passProps }, moduleBundler, resourceRegistry, viewContext)
|
|
52
62
|
.then(({ html, props = {}, markup: { links = [] } = { links: [] }, cache: { ttl } = {}, }) => {
|
|
53
63
|
// Keep track of the shortest TTL from all getPageData hooks
|
|
54
64
|
pageTtl = shortestTtl(ttl, pageTtl);
|
|
55
65
|
// Add the props id to the HTML for the custom element
|
|
56
66
|
// eg: <some-cmp> -> <some-cmp data-lwr-props-id="1234">
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
let propsAttr = '';
|
|
68
|
+
if (hydrate) {
|
|
69
|
+
// Only serialize props for custom elements that are to be hydrated
|
|
70
|
+
const propsId = getPropsId();
|
|
71
|
+
propsAttr = ` ${SSR_PROPS_ATTR}="${propsId}"`;
|
|
72
|
+
ssrProps[propsId] = props;
|
|
73
|
+
}
|
|
59
74
|
const [, remain] = html.split(`<${tagName}`);
|
|
60
|
-
html = [`<${tagName}`,
|
|
75
|
+
html = [`<${tagName}`, propsAttr, remain].join('');
|
|
76
|
+
// Create HTML <link> strings for each item in the links array
|
|
61
77
|
links.forEach(({ href, rel, as, fetchpriority }) => {
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
const relStr = rel ? ` rel="${rel}"` : '', asStr = as ? ` as="${as}"` : '', fetchStr = fetchpriority
|
|
79
|
+
? ` fetchpriority="${fetchpriority}"`
|
|
80
|
+
: '';
|
|
64
81
|
ssrLinks += `<link href="${href}"${relStr}${asStr}${fetchStr}>\n`;
|
|
65
82
|
});
|
|
66
83
|
// Overwrite the custom element with the SSRed component string
|
|
67
84
|
stringBuilder.overwrite(startOffset, endOffset, html);
|
|
68
85
|
})
|
|
69
86
|
.catch((err) => {
|
|
70
|
-
|
|
87
|
+
// Fallback to CSR by adding lwr:hydrate="client-only" to the custom element
|
|
88
|
+
// This ENSURES the component's JavaScript gets sent to the client for CSRing
|
|
89
|
+
customElements[index].props === undefined
|
|
90
|
+
? (customElements[index].props = {
|
|
91
|
+
[HYDRATE_DIRECTIVE]: HYDRATE_CLIENT_VALUE,
|
|
92
|
+
})
|
|
93
|
+
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
94
|
+
// @ts-ignore - TS thinks that props may still be undefined
|
|
95
|
+
(customElements[index].props[HYDRATE_DIRECTIVE] = HYDRATE_CLIENT_VALUE);
|
|
96
|
+
logger.warn(`Server-side rendering for "${specifier}" failed. Falling back to client-side rendering. Reason: `, err.stack);
|
|
71
97
|
});
|
|
72
98
|
}));
|
|
73
99
|
if (Object.keys(ssrProps).length) {
|
|
@@ -85,7 +111,7 @@ export default function lwcSsrViewTranformer(options, { moduleBundler, resourceR
|
|
|
85
111
|
logger.error('Adding links during server-side rendering failed. Could not find the </head> tag.');
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
|
-
logger.verbose('
|
|
114
|
+
logger.verbose('lwcSsrViewTransformer response', stringBuilder);
|
|
89
115
|
return { cache: { ttl: pageTtl } };
|
|
90
116
|
},
|
|
91
117
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { getTracer } from '@lwrjs/instrumentation';
|
|
1
2
|
import createVirtualEnvironment from '@locker/near-membrane-node';
|
|
2
3
|
import fetch from 'node-fetch';
|
|
4
|
+
import { webcrypto as crypto } from 'crypto';
|
|
3
5
|
export default function runCode(codes, context) {
|
|
4
6
|
return new Promise((resolve, reject) => {
|
|
5
7
|
let resolver;
|
|
@@ -7,8 +9,13 @@ export default function runCode(codes, context) {
|
|
|
7
9
|
function getContext() {
|
|
8
10
|
return context;
|
|
9
11
|
}
|
|
12
|
+
const tracer = getTracer();
|
|
13
|
+
const trace = tracer.trace.bind(tracer);
|
|
10
14
|
const endowments = Object.getOwnPropertyDescriptors({
|
|
15
|
+
AbortController,
|
|
16
|
+
crypto,
|
|
11
17
|
getContext,
|
|
18
|
+
trace,
|
|
12
19
|
fetch,
|
|
13
20
|
resolver,
|
|
14
21
|
// for AMD loader ModuleRegistry
|
|
@@ -15,7 +15,10 @@ const WORKER_CODE_SANDBOX_APIS = [
|
|
|
15
15
|
`const { parentPort, workerData } = require('worker_threads');`,
|
|
16
16
|
`globalThis.getContext = () => workerData;`,
|
|
17
17
|
`globalThis.fetch = require('node-fetch');`,
|
|
18
|
+
`globalThis.crypto = require('crypto').webcrypto;`,
|
|
18
19
|
`globalThis.resolver = (...args) => parentPort.postMessage(...args);`,
|
|
20
|
+
// TODO: implement tracing with worker thread
|
|
21
|
+
`globalThis.trace = (id, fn) => fn()`,
|
|
19
22
|
];
|
|
20
23
|
/**
|
|
21
24
|
* Run the SSR module code in a worker, and return the results to the main thread.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json, PageDataResponse, PublicModuleBundler, PublicResourceRegistry,
|
|
1
|
+
import type { Json, PageDataResponse, PublicModuleBundler, PublicResourceRegistry, ViewTransformPluginContext } from '@lwrjs/types';
|
|
2
2
|
interface SsrResults extends PageDataResponse {
|
|
3
3
|
html: string;
|
|
4
4
|
}
|
|
@@ -13,6 +13,6 @@ interface SsrResults extends PageDataResponse {
|
|
|
13
13
|
export declare function ssrElement({ specifier, props: templateProps }: {
|
|
14
14
|
specifier: string;
|
|
15
15
|
props: Json;
|
|
16
|
-
}, moduleBundler: PublicModuleBundler, resourceRegistry: PublicResourceRegistry, { runtimeEnvironment, runtimeParams }:
|
|
16
|
+
}, moduleBundler: PublicModuleBundler, resourceRegistry: PublicResourceRegistry, { runtimeEnvironment, runtimeParams }: ViewTransformPluginContext): Promise<SsrResults>;
|
|
17
17
|
export {};
|
|
18
18
|
//# sourceMappingURL=ssr-element.d.ts.map
|
|
@@ -2,6 +2,8 @@ import { getBundle, getCode } from './amd-utils.js';
|
|
|
2
2
|
import runCode from './sandbox.js';
|
|
3
3
|
import { performance } from 'perf_hooks';
|
|
4
4
|
import { logger } from '@lwrjs/shared-utils';
|
|
5
|
+
import { getTracer, ViewSpan } from '@lwrjs/instrumentation';
|
|
6
|
+
const SSR_FLAG = 'process.env.SSR = true;';
|
|
5
7
|
/**
|
|
6
8
|
* Create a bundle for the given SSR module and run the code in a sandbox.
|
|
7
9
|
* @param moduleInfo - specifier: The ID of the module, generated by "lwc-ssr/moduleProvider", which SSRs a component
|
|
@@ -34,17 +36,25 @@ export async function ssrElement({ specifier, props: templateProps }, moduleBund
|
|
|
34
36
|
};
|
|
35
37
|
// Get the SSR string and properties bag
|
|
36
38
|
const startTime = performance.now();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
return getTracer().trace({
|
|
40
|
+
name: ViewSpan.ServerSideRender,
|
|
41
|
+
attributes: {
|
|
42
|
+
specifier: bundleSpecifier,
|
|
43
|
+
},
|
|
44
|
+
}, async () => {
|
|
45
|
+
const { result, props, markup, cache, error } = format === 'amd'
|
|
46
|
+
? await runCode([
|
|
47
|
+
SSR_FLAG,
|
|
48
|
+
...(await getCode(runtimeEnvironment, version.replace(/\./g, '_'), bundleSpecifier, bundleRecord.includedModules, resourceRegistry)),
|
|
49
|
+
code,
|
|
50
|
+
], context)
|
|
51
|
+
: await runCode([SSR_FLAG, code], context);
|
|
52
|
+
const endTime = performance.now();
|
|
53
|
+
const timeDiff = endTime - startTime;
|
|
54
|
+
logger.info(`[Server-side Rendering] ${specifier} in ${Math.round(timeDiff)} ms`);
|
|
55
|
+
if (error)
|
|
56
|
+
throw new Error(error);
|
|
57
|
+
return { html: result, props, markup, cache };
|
|
58
|
+
});
|
|
49
59
|
}
|
|
50
60
|
//# 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.10.0
|
|
7
|
+
"version": "0.10.0",
|
|
8
8
|
"homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -33,16 +33,17 @@
|
|
|
33
33
|
"build/**/*.d.ts"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@locker/near-membrane-node": "^0.12.
|
|
37
|
-
"@lwrjs/diagnostics": "0.10.0
|
|
38
|
-
"@lwrjs/
|
|
36
|
+
"@locker/near-membrane-node": "^0.12.15",
|
|
37
|
+
"@lwrjs/diagnostics": "0.10.0",
|
|
38
|
+
"@lwrjs/instrumentation": "0.10.0",
|
|
39
|
+
"@lwrjs/shared-utils": "0.10.0",
|
|
39
40
|
"node-fetch": "^2.6.8"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@lwrjs/types": "0.10.0
|
|
43
|
+
"@lwrjs/types": "0.10.0"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
45
|
-
"node": ">=16.0.0
|
|
46
|
+
"node": ">=16.0.0"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "e6deaeef3db8aa079acefed508897eca19b3218a"
|
|
48
49
|
}
|