@happy-dom/server-renderer 20.0.11 → 20.1.1
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/lib/ServerRenderer.d.ts +3 -3
- package/lib/ServerRenderer.d.ts.map +1 -1
- package/lib/ServerRenderer.js +27 -13
- package/lib/ServerRenderer.js.map +1 -1
- package/lib/ServerRendererBrowser.d.ts.map +1 -1
- package/lib/ServerRendererBrowser.js +7 -133
- package/lib/ServerRendererBrowser.js.map +1 -1
- package/lib/{ServerRendererWorker.d.ts → ServerRendererBrowserWorker.d.ts} +2 -2
- package/lib/ServerRendererBrowserWorker.d.ts.map +1 -0
- package/lib/{ServerRendererWorker.js → ServerRendererBrowserWorker.js} +3 -3
- package/lib/ServerRendererBrowserWorker.js.map +1 -0
- package/lib/ServerRendererPage.d.ts +24 -0
- package/lib/ServerRendererPage.d.ts.map +1 -0
- package/lib/ServerRendererPage.js +234 -0
- package/lib/ServerRendererPage.js.map +1 -0
- package/lib/ServerRendererPageWorker.d.ts +15 -0
- package/lib/ServerRendererPageWorker.d.ts.map +1 -0
- package/lib/ServerRendererPageWorker.js +38 -0
- package/lib/ServerRendererPageWorker.js.map +1 -0
- package/lib/ServerRendererServer.d.ts.map +1 -1
- package/lib/ServerRendererServer.js +0 -4
- package/lib/ServerRendererServer.js.map +1 -1
- package/lib/config/DefaultServerRendererConfiguration.d.ts.map +1 -1
- package/lib/config/DefaultServerRendererConfiguration.js +6 -6
- package/lib/config/DefaultServerRendererConfiguration.js.map +1 -1
- package/lib/enums/ServerRendererModeEnum.d.ts +6 -0
- package/lib/enums/ServerRendererModeEnum.d.ts.map +1 -0
- package/lib/enums/ServerRendererModeEnum.js +9 -0
- package/lib/enums/ServerRendererModeEnum.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/types/IOptionalServerRendererConfiguration.d.ts +11 -2
- package/lib/types/IOptionalServerRendererConfiguration.d.ts.map +1 -1
- package/lib/types/IServerRendererConfiguration.d.ts +11 -2
- package/lib/types/IServerRendererConfiguration.d.ts.map +1 -1
- package/lib/types/IServerRendererItem.d.ts +2 -1
- package/lib/types/IServerRendererItem.d.ts.map +1 -1
- package/lib/types/IServerRendererResult.d.ts +4 -4
- package/lib/types/IServerRendererResult.d.ts.map +1 -1
- package/lib/utilities/BrowserWindowPolyfill.d.ts.map +1 -1
- package/lib/utilities/BrowserWindowPolyfill.js +2 -2
- package/lib/utilities/BrowserWindowPolyfill.js.map +1 -1
- package/lib/utilities/HelpPrinterRows.d.ts.map +1 -1
- package/lib/utilities/HelpPrinterRows.js +25 -4
- package/lib/utilities/HelpPrinterRows.js.map +1 -1
- package/lib/utilities/ProcessArgumentsParser.d.ts.map +1 -1
- package/lib/utilities/ProcessArgumentsParser.js +86 -22
- package/lib/utilities/ProcessArgumentsParser.js.map +1 -1
- package/lib/utilities/ServerRendererConfigurationFactory.js +1 -1
- package/lib/utilities/ServerRendererConfigurationFactory.js.map +1 -1
- package/package.json +3 -2
- package/src/ServerRenderer.ts +36 -13
- package/src/ServerRendererBrowser.ts +7 -151
- package/src/{ServerRendererWorker.ts → ServerRendererBrowserWorker.ts} +2 -2
- package/src/ServerRendererPage.ts +279 -0
- package/src/ServerRendererPageWorker.ts +55 -0
- package/src/ServerRendererServer.ts +3 -8
- package/src/config/DefaultServerRendererConfiguration.ts +6 -6
- package/src/enums/ServerRendererModeEnum.ts +8 -0
- package/src/index.ts +3 -1
- package/src/types/IOptionalServerRendererConfiguration.ts +21 -2
- package/src/types/IServerRendererConfiguration.ts +20 -2
- package/src/types/IServerRendererItem.ts +2 -1
- package/src/types/IServerRendererResult.ts +4 -4
- package/src/utilities/BrowserWindowPolyfill.ts +3 -2
- package/src/utilities/HelpPrinterRows.ts +25 -4
- package/src/utilities/ProcessArgumentsParser.ts +93 -24
- package/src/utilities/ServerRendererConfigurationFactory.ts +1 -1
- package/lib/ServerRendererWorker.d.ts.map +0 -1
- package/lib/ServerRendererWorker.js.map +0 -1
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import Browser from 'happy-dom/lib/browser/Browser.js';
|
|
2
|
-
import HTMLSerializer from 'happy-dom/lib/html-serializer/HTMLSerializer.js';
|
|
3
2
|
import IServerRendererConfiguration from './types/IServerRendererConfiguration.js';
|
|
4
|
-
import FS from 'fs';
|
|
5
|
-
import Path from 'path';
|
|
6
3
|
import IServerRendererItem from './types/IServerRendererItem.js';
|
|
7
4
|
import IServerRendererResult from './types/IServerRendererResult.js';
|
|
8
|
-
import
|
|
9
|
-
import BrowserWindowPolyfill from './utilities/BrowserWindowPolyfill.js';
|
|
5
|
+
import ServerRendererPage from './ServerRendererPage.js';
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Server renderer browser.
|
|
@@ -15,7 +11,7 @@ export default class ServerRendererBrowser {
|
|
|
15
11
|
#configuration: IServerRendererConfiguration;
|
|
16
12
|
#browser: Browser;
|
|
17
13
|
#isCacheLoaded: boolean = false;
|
|
18
|
-
#
|
|
14
|
+
#pageRenderer: ServerRendererPage;
|
|
19
15
|
|
|
20
16
|
/**
|
|
21
17
|
* Constructor.
|
|
@@ -35,6 +31,7 @@ export default class ServerRendererBrowser {
|
|
|
35
31
|
}
|
|
36
32
|
: configuration.browser;
|
|
37
33
|
this.#browser = new Browser({ settings });
|
|
34
|
+
this.#pageRenderer = new ServerRendererPage(configuration);
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
@@ -56,7 +53,7 @@ export default class ServerRendererBrowser {
|
|
|
56
53
|
const promises = [];
|
|
57
54
|
for (const url of chunk) {
|
|
58
55
|
promises.push(
|
|
59
|
-
this.#
|
|
56
|
+
this.#renderPage(browser, url).then((result) => {
|
|
60
57
|
results.push(result);
|
|
61
58
|
})
|
|
62
59
|
);
|
|
@@ -66,7 +63,7 @@ export default class ServerRendererBrowser {
|
|
|
66
63
|
} else {
|
|
67
64
|
const promises = [];
|
|
68
65
|
for (const url of items) {
|
|
69
|
-
promises.push(this.#
|
|
66
|
+
promises.push(this.#renderPage(browser, url));
|
|
70
67
|
}
|
|
71
68
|
results = await Promise.all(promises);
|
|
72
69
|
}
|
|
@@ -91,7 +88,7 @@ export default class ServerRendererBrowser {
|
|
|
91
88
|
* @param browser Browser.
|
|
92
89
|
* @param item Item.
|
|
93
90
|
*/
|
|
94
|
-
async #
|
|
91
|
+
async #renderPage(browser: Browser, item: IServerRendererItem): Promise<IServerRendererResult> {
|
|
95
92
|
const responseCache = browser.defaultContext.responseCache;
|
|
96
93
|
const preflightResponseCache = browser.defaultContext.preflightResponseCache;
|
|
97
94
|
const configuration = this.#configuration;
|
|
@@ -108,148 +105,7 @@ export default class ServerRendererBrowser {
|
|
|
108
105
|
context.preflightResponseCache = preflightResponseCache;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
const response = (await page.goto(item.url, {
|
|
113
|
-
timeout: configuration.render.timeout,
|
|
114
|
-
headers: item.headers,
|
|
115
|
-
beforeContentCallback(window) {
|
|
116
|
-
window.addEventListener('error', (event) => {
|
|
117
|
-
if ((<ErrorEvent>event).error) {
|
|
118
|
-
pageErrors.push((<ErrorEvent>event).error!.stack!);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
if (!configuration.render.disablePolyfills) {
|
|
122
|
-
BrowserWindowPolyfill.applyPolyfills(window);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}))!;
|
|
126
|
-
|
|
127
|
-
const headers: { [key: string]: string } = {};
|
|
128
|
-
|
|
129
|
-
for (const [key, value] of response.headers) {
|
|
130
|
-
if (key !== 'content-encoding') {
|
|
131
|
-
headers[key] = value;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
137
|
-
await page.close();
|
|
138
|
-
return {
|
|
139
|
-
url: item.url,
|
|
140
|
-
content: null,
|
|
141
|
-
status: response.status,
|
|
142
|
-
statusText: response.statusText,
|
|
143
|
-
headers,
|
|
144
|
-
outputFile: item.outputFile ?? null,
|
|
145
|
-
error: `Failed to render page ${item.url} (${response.status} ${response.statusText})`,
|
|
146
|
-
pageConsole,
|
|
147
|
-
pageErrors
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let timeoutError: string | null = null;
|
|
152
|
-
const timeout =
|
|
153
|
-
this.#browser.settings.debug.traceWaitUntilComplete === -1
|
|
154
|
-
? setTimeout(() => {
|
|
155
|
-
timeoutError = `The page was not rendered within the defined time of ${configuration.render.timeout}ms and the operation was aborted. You can increase this value with the "render.timeout" setting.\n\nThe page may contain scripts with timer loops that prevent it from completing. You can debug open handles by setting "debug" to true, or prevent timer loops by setting "browser.timer.preventTimerLoops" to true. Read more about this in the documentation.`;
|
|
156
|
-
page.abort();
|
|
157
|
-
}, configuration.render.timeout)
|
|
158
|
-
: null;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
await page.waitUntilComplete();
|
|
162
|
-
} catch (error) {
|
|
163
|
-
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
164
|
-
await page.close();
|
|
165
|
-
return {
|
|
166
|
-
url: item.url,
|
|
167
|
-
content: null,
|
|
168
|
-
status: response.status,
|
|
169
|
-
statusText: response.statusText,
|
|
170
|
-
headers,
|
|
171
|
-
outputFile: item.outputFile ?? null,
|
|
172
|
-
error: (<Error>error).stack!,
|
|
173
|
-
pageConsole,
|
|
174
|
-
pageErrors
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Wait for errors to be printed
|
|
179
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
180
|
-
|
|
181
|
-
if (timeout) {
|
|
182
|
-
clearTimeout(timeout);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
186
|
-
|
|
187
|
-
if (timeoutError) {
|
|
188
|
-
page.close();
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
url: item.url,
|
|
192
|
-
content: null,
|
|
193
|
-
status: response.status,
|
|
194
|
-
statusText: response.statusText,
|
|
195
|
-
headers,
|
|
196
|
-
outputFile: null,
|
|
197
|
-
error: timeoutError,
|
|
198
|
-
pageConsole,
|
|
199
|
-
pageErrors
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const serializer = new HTMLSerializer({
|
|
204
|
-
allShadowRoots: configuration.render.allShadowRoots,
|
|
205
|
-
serializableShadowRoots: configuration.render.serializableShadowRoots,
|
|
206
|
-
excludeShadowRootTags: configuration.render.excludeShadowRootTags
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const result = serializer.serializeToString(page.mainFrame.document);
|
|
210
|
-
|
|
211
|
-
await page.close();
|
|
212
|
-
|
|
213
|
-
if (!item.outputFile) {
|
|
214
|
-
return {
|
|
215
|
-
url: item.url,
|
|
216
|
-
content: result,
|
|
217
|
-
status: response.status,
|
|
218
|
-
statusText: response.statusText,
|
|
219
|
-
headers,
|
|
220
|
-
outputFile: null,
|
|
221
|
-
error: null,
|
|
222
|
-
pageConsole,
|
|
223
|
-
pageErrors
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const directory = Path.dirname(item.outputFile);
|
|
228
|
-
|
|
229
|
-
if (!this.#createdDirectories.has(directory)) {
|
|
230
|
-
this.#createdDirectories.add(directory);
|
|
231
|
-
try {
|
|
232
|
-
await FS.promises.mkdir(directory, {
|
|
233
|
-
recursive: true
|
|
234
|
-
});
|
|
235
|
-
} catch {
|
|
236
|
-
// Ignore
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
await FS.promises.writeFile(item.outputFile, result);
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
url: item.url,
|
|
244
|
-
content: null,
|
|
245
|
-
status: response.status,
|
|
246
|
-
statusText: response.statusText,
|
|
247
|
-
headers,
|
|
248
|
-
outputFile: item.outputFile,
|
|
249
|
-
error: null,
|
|
250
|
-
pageConsole,
|
|
251
|
-
pageErrors
|
|
252
|
-
};
|
|
108
|
+
return this.#pageRenderer.render(page, item);
|
|
253
109
|
}
|
|
254
110
|
|
|
255
111
|
/**
|
|
@@ -5,7 +5,7 @@ import Inspector from 'node:inspector';
|
|
|
5
5
|
/**
|
|
6
6
|
* Server renderer worker.
|
|
7
7
|
*/
|
|
8
|
-
export default class
|
|
8
|
+
export default class ServerRendererBrowserWorker {
|
|
9
9
|
/**
|
|
10
10
|
* Connects to the worker.
|
|
11
11
|
*/
|
|
@@ -26,4 +26,4 @@ export default class ServerRendererWorker {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
ServerRendererBrowserWorker.connect();
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import HTMLSerializer from 'happy-dom/lib/html-serializer/HTMLSerializer.js';
|
|
2
|
+
import FS from 'fs';
|
|
3
|
+
import Path from 'path';
|
|
4
|
+
import IServerRendererItem from './types/IServerRendererItem.js';
|
|
5
|
+
import IServerRendererResult from './types/IServerRendererResult.js';
|
|
6
|
+
import { ErrorEvent, IBrowserPage, Response } from 'happy-dom';
|
|
7
|
+
import BrowserWindowPolyfill from './utilities/BrowserWindowPolyfill.js';
|
|
8
|
+
import IServerRendererConfiguration from './types/IServerRendererConfiguration.js';
|
|
9
|
+
import ServerRendererModeEnum from './enums/ServerRendererModeEnum.js';
|
|
10
|
+
|
|
11
|
+
const SET_TIMEOUT = setTimeout;
|
|
12
|
+
const CLEAR_TIMEOUT = clearTimeout;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Server renderer page.
|
|
16
|
+
*/
|
|
17
|
+
export default class ServerRendererPage {
|
|
18
|
+
#configuration: IServerRendererConfiguration;
|
|
19
|
+
#createdDirectories: Set<string> = new Set();
|
|
20
|
+
#initialized: boolean = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Constructor.
|
|
24
|
+
*
|
|
25
|
+
* @param configuration Configuration.
|
|
26
|
+
*/
|
|
27
|
+
constructor(configuration: IServerRendererConfiguration) {
|
|
28
|
+
this.#configuration = configuration;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Renders a page.
|
|
33
|
+
*
|
|
34
|
+
* @param page Browser page.
|
|
35
|
+
* @param item Item.
|
|
36
|
+
*/
|
|
37
|
+
public async render(
|
|
38
|
+
page: IBrowserPage,
|
|
39
|
+
item: IServerRendererItem
|
|
40
|
+
): Promise<IServerRendererResult> {
|
|
41
|
+
const configuration = this.#configuration;
|
|
42
|
+
const pageErrors: string[] = [];
|
|
43
|
+
const errorListener = (event: any): void => {
|
|
44
|
+
if ((<ErrorEvent>event).error) {
|
|
45
|
+
pageErrors.push((<ErrorEvent>event).error!.stack!);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
let response: Response | null = null;
|
|
49
|
+
let headers: { [key: string]: string } | null = null;
|
|
50
|
+
|
|
51
|
+
if (item.html) {
|
|
52
|
+
if (item.url) {
|
|
53
|
+
page.mainFrame.url = item.url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
page.mainFrame.window.addEventListener('error', errorListener);
|
|
57
|
+
|
|
58
|
+
if (!this.#initialized) {
|
|
59
|
+
this.#initialized = true;
|
|
60
|
+
|
|
61
|
+
if (!configuration.render.disablePolyfills) {
|
|
62
|
+
BrowserWindowPolyfill.applyPolyfills(page.mainFrame.window);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (configuration.render.setupScript) {
|
|
66
|
+
page.mainFrame.evaluateModule({ code: configuration.render.setupScript });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
page.mainFrame.document.open();
|
|
71
|
+
page.mainFrame.document.write(item.html);
|
|
72
|
+
} else if (item.url) {
|
|
73
|
+
headers = {};
|
|
74
|
+
|
|
75
|
+
if (configuration.render.mode === ServerRendererModeEnum.page) {
|
|
76
|
+
response = await page.mainFrame.window.fetch(item.url, {
|
|
77
|
+
headers: item.headers || {}
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
response = (await page.goto(item.url, {
|
|
81
|
+
timeout: configuration.render.timeout,
|
|
82
|
+
headers: item.headers,
|
|
83
|
+
beforeContentCallback(window) {
|
|
84
|
+
window.addEventListener('error', errorListener);
|
|
85
|
+
|
|
86
|
+
if (!configuration.render.disablePolyfills) {
|
|
87
|
+
BrowserWindowPolyfill.applyPolyfills(window);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (configuration.render.setupScript) {
|
|
91
|
+
page.evaluateModule({ code: configuration.render.setupScript });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}))!;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [key, value] of response.headers) {
|
|
98
|
+
if (key !== 'content-encoding') {
|
|
99
|
+
headers[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
105
|
+
|
|
106
|
+
if (configuration.render.mode !== ServerRendererModeEnum.page) {
|
|
107
|
+
await page.close();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
url: item.url,
|
|
112
|
+
content: null,
|
|
113
|
+
status: response.status,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
headers,
|
|
116
|
+
outputFile: item.outputFile ?? null,
|
|
117
|
+
error: `Failed to render page ${item.url} (${response.status} ${response.statusText})`,
|
|
118
|
+
pageConsole,
|
|
119
|
+
pageErrors
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (configuration.render.mode === ServerRendererModeEnum.page) {
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
|
|
126
|
+
page.mainFrame.window.addEventListener('error', errorListener);
|
|
127
|
+
|
|
128
|
+
if (!this.#initialized) {
|
|
129
|
+
this.#initialized = true;
|
|
130
|
+
if (!configuration.render.disablePolyfills) {
|
|
131
|
+
BrowserWindowPolyfill.applyPolyfills(page.mainFrame.window);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (configuration.render.setupScript) {
|
|
135
|
+
page.mainFrame.evaluateModule({ code: configuration.render.setupScript });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
page.mainFrame.document.open();
|
|
140
|
+
page.mainFrame.document.write(text);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
144
|
+
|
|
145
|
+
if (configuration.render.mode !== ServerRendererModeEnum.page) {
|
|
146
|
+
await page.close();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
url: null,
|
|
151
|
+
content: null,
|
|
152
|
+
status: null,
|
|
153
|
+
statusText: null,
|
|
154
|
+
headers: null,
|
|
155
|
+
outputFile: item.outputFile ?? null,
|
|
156
|
+
error: `No "url" or "html" provided to render.`,
|
|
157
|
+
pageConsole,
|
|
158
|
+
pageErrors
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let timeoutError: string | null = null;
|
|
163
|
+
const timeout =
|
|
164
|
+
configuration.browser.debug.traceWaitUntilComplete === -1
|
|
165
|
+
? SET_TIMEOUT(() => {
|
|
166
|
+
timeoutError = `The page was not rendered within the defined time of ${configuration.render.timeout}ms and the operation was aborted. You can increase this value with the "render.timeout" setting.\n\nThe page may contain scripts with timer loops that prevent it from completing. You can debug open handles by setting "debug" to true, or prevent timer loops by setting "browser.timer.preventTimerLoops" to true. Read more about this in the documentation.`;
|
|
167
|
+
page.abort();
|
|
168
|
+
}, configuration.render.timeout)
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await page.waitUntilComplete();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
175
|
+
const url = item.url || page.url;
|
|
176
|
+
|
|
177
|
+
if (configuration.render.mode !== ServerRendererModeEnum.page) {
|
|
178
|
+
await page.close();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
url,
|
|
183
|
+
content: null,
|
|
184
|
+
status: response?.status || null,
|
|
185
|
+
statusText: response?.statusText || null,
|
|
186
|
+
headers,
|
|
187
|
+
outputFile: item.outputFile ?? null,
|
|
188
|
+
error: (<Error>error).stack!,
|
|
189
|
+
pageConsole,
|
|
190
|
+
pageErrors
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Wait for errors to be printed
|
|
195
|
+
await new Promise((resolve) => SET_TIMEOUT(resolve, 10));
|
|
196
|
+
|
|
197
|
+
page.mainFrame.window.removeEventListener('error', errorListener);
|
|
198
|
+
|
|
199
|
+
if (timeout) {
|
|
200
|
+
CLEAR_TIMEOUT(timeout);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pageConsole = page.virtualConsolePrinter.readAsString();
|
|
204
|
+
|
|
205
|
+
if (timeoutError) {
|
|
206
|
+
const url = item.url || page.url;
|
|
207
|
+
|
|
208
|
+
if (configuration.render.mode !== ServerRendererModeEnum.page) {
|
|
209
|
+
await page.close();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
url,
|
|
214
|
+
content: null,
|
|
215
|
+
status: response?.status || null,
|
|
216
|
+
statusText: response?.statusText || null,
|
|
217
|
+
headers,
|
|
218
|
+
outputFile: null,
|
|
219
|
+
error: timeoutError,
|
|
220
|
+
pageConsole,
|
|
221
|
+
pageErrors
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const serializer = new HTMLSerializer({
|
|
226
|
+
allShadowRoots: configuration.render.allShadowRoots,
|
|
227
|
+
serializableShadowRoots: configuration.render.serializableShadowRoots,
|
|
228
|
+
excludeShadowRootTags: configuration.render.excludeShadowRootTags
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = serializer.serializeToString(page.mainFrame.document);
|
|
232
|
+
const url = item.url || page.url;
|
|
233
|
+
|
|
234
|
+
if (configuration.render.mode !== ServerRendererModeEnum.page) {
|
|
235
|
+
await page.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!item.outputFile) {
|
|
239
|
+
return {
|
|
240
|
+
url,
|
|
241
|
+
content: result,
|
|
242
|
+
status: response?.status || null,
|
|
243
|
+
statusText: response?.statusText || null,
|
|
244
|
+
headers,
|
|
245
|
+
outputFile: null,
|
|
246
|
+
error: null,
|
|
247
|
+
pageConsole,
|
|
248
|
+
pageErrors
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const directory = Path.dirname(item.outputFile);
|
|
253
|
+
|
|
254
|
+
if (!this.#createdDirectories.has(directory)) {
|
|
255
|
+
this.#createdDirectories.add(directory);
|
|
256
|
+
try {
|
|
257
|
+
await FS.promises.mkdir(directory, {
|
|
258
|
+
recursive: true
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await FS.promises.writeFile(item.outputFile, result);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
url,
|
|
269
|
+
content: null,
|
|
270
|
+
status: response?.status || null,
|
|
271
|
+
statusText: response?.statusText || null,
|
|
272
|
+
headers,
|
|
273
|
+
outputFile: item.outputFile,
|
|
274
|
+
error: null,
|
|
275
|
+
pageConsole,
|
|
276
|
+
pageErrors
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
+
import Inspector from 'node:inspector';
|
|
3
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
4
|
+
import { Document, Window } from 'happy-dom';
|
|
5
|
+
import WindowBrowserContext from 'happy-dom/lib/window/WindowBrowserContext.js';
|
|
6
|
+
import ServerRendererPage from './ServerRendererPage.js';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
const document: Document;
|
|
10
|
+
const window: Window;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Server renderer worker.
|
|
15
|
+
*/
|
|
16
|
+
export default class ServerRendererPageWorker {
|
|
17
|
+
/**
|
|
18
|
+
* Connects to the worker.
|
|
19
|
+
*/
|
|
20
|
+
public static async connect(): Promise<void> {
|
|
21
|
+
const { configuration } = workerData;
|
|
22
|
+
|
|
23
|
+
if (configuration.inspect) {
|
|
24
|
+
Inspector.open();
|
|
25
|
+
Inspector.waitForDebugger();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
GlobalRegistrator.register({
|
|
29
|
+
url: 'https://localhost:8080/',
|
|
30
|
+
settings: configuration.browser
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const renderer = new ServerRendererPage(configuration);
|
|
34
|
+
|
|
35
|
+
parentPort?.on('message', async ({ items }) => {
|
|
36
|
+
if (items.length !== 1) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Failed to render page worker. Page workers can only render one item at a time.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const page = new WindowBrowserContext(window).getBrowserPage();
|
|
43
|
+
|
|
44
|
+
if (!page) {
|
|
45
|
+
throw new Error('Failed to render page. No browser page available.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await renderer.render(page, items[0]);
|
|
49
|
+
|
|
50
|
+
parentPort?.postMessage({ status: 'done', results: [result] });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ServerRendererPageWorker.connect();
|
|
@@ -197,11 +197,6 @@ export default class ServerRendererServer {
|
|
|
197
197
|
result = (
|
|
198
198
|
await this.#serverRenderer.render([{ url: url.href, headers }], { keepAlive: true })
|
|
199
199
|
)[0];
|
|
200
|
-
|
|
201
|
-
if (this.#configuration.logLevel >= ServerRendererLogLevelEnum.info) {
|
|
202
|
-
// eslint-disable-next-line no-console
|
|
203
|
-
console.log(Chalk.bold(`• Rendered ${url.href}`));
|
|
204
|
-
}
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
if (isCacheQueueEnabled) {
|
|
@@ -215,18 +210,18 @@ export default class ServerRendererServer {
|
|
|
215
210
|
}
|
|
216
211
|
}
|
|
217
212
|
|
|
218
|
-
for (const key of Object.keys(result.headers)) {
|
|
213
|
+
for (const key of Object.keys(result.headers!)) {
|
|
219
214
|
const lowerKey = key.toLowerCase();
|
|
220
215
|
if (
|
|
221
216
|
lowerKey !== 'transfer-encoding' &&
|
|
222
217
|
lowerKey !== 'content-length' &&
|
|
223
218
|
lowerKey !== 'content-encoding'
|
|
224
219
|
) {
|
|
225
|
-
response.setHeader(key, result.headers[key]);
|
|
220
|
+
response.setHeader(key, result.headers![key]);
|
|
226
221
|
}
|
|
227
222
|
}
|
|
228
223
|
|
|
229
|
-
response.statusCode = result.error ? 500 : result.status
|
|
224
|
+
response.statusCode = result.error ? 500 : result.status!;
|
|
230
225
|
|
|
231
226
|
if (result.error) {
|
|
232
227
|
if (this.#configuration.logLevel >= ServerRendererLogLevelEnum.error) {
|
|
@@ -3,14 +3,12 @@ import ServerRendererLogLevelEnum from '../enums/ServerRendererLogLevelEnum.js';
|
|
|
3
3
|
import type IServerRendererConfiguration from '../types/IServerRendererConfiguration.js';
|
|
4
4
|
import OS from 'os';
|
|
5
5
|
import { BrowserErrorCaptureEnum } from 'happy-dom';
|
|
6
|
+
import ServerRendererModeEnum from '../enums/ServerRendererModeEnum.js';
|
|
6
7
|
|
|
7
8
|
export default <IServerRendererConfiguration>{
|
|
8
9
|
browser: {
|
|
9
10
|
...DefaultBrowserSettings,
|
|
10
|
-
errorCapture: BrowserErrorCaptureEnum.processLevel
|
|
11
|
-
// This is enabled by default as the entire point of this package is to server-render client side JavaScript.
|
|
12
|
-
// "--disallow-code-generation-from-strings" is enabled on workers to prevent escape of the VM context.
|
|
13
|
-
enableJavaScriptEvaluation: true
|
|
11
|
+
errorCapture: BrowserErrorCaptureEnum.processLevel
|
|
14
12
|
},
|
|
15
13
|
outputDirectory: './happy-dom/render',
|
|
16
14
|
logLevel: ServerRendererLogLevelEnum.info,
|
|
@@ -33,9 +31,11 @@ export default <IServerRendererConfiguration>{
|
|
|
33
31
|
serializableShadowRoots: false,
|
|
34
32
|
allShadowRoots: false,
|
|
35
33
|
excludeShadowRootTags: null,
|
|
36
|
-
disablePolyfills: false
|
|
34
|
+
disablePolyfills: false,
|
|
35
|
+
setupScript: null,
|
|
36
|
+
mode: ServerRendererModeEnum.browser
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
renderItems: null,
|
|
39
39
|
server: {
|
|
40
40
|
start: false,
|
|
41
41
|
serverURL: 'https://localhost:3000',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
enum ServerRendererModeEnum {
|
|
2
|
+
// Use a Browser instance in each worker to render pages.
|
|
3
|
+
browser = 'browser',
|
|
4
|
+
// Render a single page in each worker without using VM isolation which may not be supported in some execution environments.
|
|
5
|
+
page = 'page'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default ServerRendererModeEnum;
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import IServerRendererResult from './types/IServerRendererResult.js';
|
|
|
5
5
|
import IServerRendererConfiguration from './types/IServerRendererConfiguration.js';
|
|
6
6
|
import IOptionalServerRendererConfiguration from './types/IOptionalServerRendererConfiguration.js';
|
|
7
7
|
import ServerRendererLogLevelEnum from './enums/ServerRendererLogLevelEnum.js';
|
|
8
|
+
import ServerRendererModeEnum from './enums/ServerRendererModeEnum.js';
|
|
8
9
|
|
|
9
10
|
export {
|
|
10
11
|
ServerRenderer,
|
|
@@ -13,5 +14,6 @@ export {
|
|
|
13
14
|
IServerRendererResult,
|
|
14
15
|
IServerRendererConfiguration,
|
|
15
16
|
IOptionalServerRendererConfiguration,
|
|
16
|
-
ServerRendererLogLevelEnum
|
|
17
|
+
ServerRendererLogLevelEnum,
|
|
18
|
+
ServerRendererModeEnum
|
|
17
19
|
};
|