dh-remixer-sdk 0.0.24-d888b2d → 0.0.24-e08afd3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dh-remixer-sdk",
|
|
3
|
-
"version": "0.0.24-
|
|
3
|
+
"version": "0.0.24-e08afd3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"templates",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"chrome-launcher": "1.2.1",
|
|
18
18
|
"chrome-remote-interface": "0.34.0",
|
|
19
19
|
"estree-walker": "3.0.3",
|
|
20
|
-
"sucrase": "3.35.0"
|
|
20
|
+
"sucrase": "3.35.0",
|
|
21
|
+
"@sparticuz/chromium": "143.0.4"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"eslint-plugin-n": "17.24.0",
|
|
@@ -32,7 +32,38 @@ async function main() {
|
|
|
32
32
|
const reactRoutes = await getRoutes();
|
|
33
33
|
let sitemapRoutes = [ROOT_ROUTE, reactRoutes.find((i) => typeof i === "object")];
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Retry logic with proper cleanup on failure
|
|
36
|
+
let prerenderer = null;
|
|
37
|
+
const maxRetries = 3;
|
|
38
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
console.log(`[ssg-helmet] Creating Prerenderer (attempt ${attempt}/${maxRetries})...`);
|
|
41
|
+
prerenderer = await Prerenderer.create();
|
|
42
|
+
console.log(`[ssg-helmet] Prerenderer created successfully`);
|
|
43
|
+
break;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`[ssg-helmet] Prerenderer.create() failed on attempt ${attempt}:`, err.message);
|
|
46
|
+
|
|
47
|
+
// Clean up any partially-created prerenderer
|
|
48
|
+
if (prerenderer) {
|
|
49
|
+
try {
|
|
50
|
+
await prerenderer.close();
|
|
51
|
+
} catch (closeErr) {
|
|
52
|
+
console.error(`[ssg-helmet] Error during cleanup:`, closeErr.message);
|
|
53
|
+
}
|
|
54
|
+
prerenderer = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (attempt === maxRetries) {
|
|
58
|
+
throw new Error(`Failed to create Prerenderer after ${maxRetries} attempts: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exponential backoff: 2s, 4s
|
|
62
|
+
const delay = attempt * 2000;
|
|
63
|
+
console.log(`[ssg-helmet] Retrying in ${delay}ms...`);
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
36
67
|
|
|
37
68
|
while (queuedRoutes.length > 0) {
|
|
38
69
|
const knownRoutes = [...queuedRoutes, ...visistedRoutes];
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { URL } from "node:url";
|
|
5
5
|
import { setTimeout } from "node:timers/promises";
|
|
6
6
|
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
7
8
|
import * as chromeLauncher from "chrome-launcher";
|
|
8
9
|
import CDP from "chrome-remote-interface";
|
|
9
10
|
import { REMIXER_BUILD_DIR } from "./vars.mjs";
|
|
@@ -15,81 +16,75 @@ export class Prerenderer {
|
|
|
15
16
|
return instance;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
#renderQueue = Promise.resolve();
|
|
20
|
+
|
|
18
21
|
async prerenderPage(route = "/") {
|
|
22
|
+
return (this.#renderQueue = this.#renderQueue.then(() =>
|
|
23
|
+
this.#renderPage(route),
|
|
24
|
+
));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async #renderPage(route) {
|
|
19
28
|
const r = this.#normalize(route);
|
|
29
|
+
const { Page, Runtime } = this.#client;
|
|
30
|
+
|
|
31
|
+
// Navigate
|
|
32
|
+
await Page.navigate({ url: `${this.#origin}${r}` });
|
|
33
|
+
await Page.loadEventFired();
|
|
34
|
+
|
|
35
|
+
// Wait for Helmet (non-fatal)
|
|
36
|
+
await Runtime.evaluate({
|
|
37
|
+
expression: `
|
|
38
|
+
(async function () {
|
|
39
|
+
const deadline = Date.now() + 15000;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
if (document.head && document.head.querySelector('[data-react-helmet="true"]')) return true;
|
|
42
|
+
await new Promise(r => setTimeout(r, 50));
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
})()
|
|
46
|
+
`,
|
|
47
|
+
awaitPromise: true,
|
|
48
|
+
returnByValue: true,
|
|
49
|
+
});
|
|
20
50
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
target = await CDP.New({ port: this.#cdpPort });
|
|
27
|
-
client = await CDP({ port: this.#cdpPort, target });
|
|
28
|
-
break;
|
|
29
|
-
} catch (e) {
|
|
30
|
-
if (attempt === 10) throw e;
|
|
31
|
-
const delay = attempt * 1500; // 1.5s, 3s, 4.5s, ..., 15s
|
|
32
|
-
console.log(`[ssg-helmet] CDP connection attempt ${attempt} failed, retrying in ${delay}ms...`);
|
|
33
|
-
await setTimeout(delay);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
51
|
+
await Runtime.evaluate({
|
|
52
|
+
expression: `new Promise(r => setTimeout(r, 100))`,
|
|
53
|
+
awaitPromise: true,
|
|
54
|
+
returnByValue: true,
|
|
55
|
+
});
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
// Snapshot
|
|
58
|
+
const { result } = await Runtime.evaluate({
|
|
59
|
+
expression: "document.documentElement.outerHTML",
|
|
60
|
+
returnByValue: true,
|
|
61
|
+
});
|
|
38
62
|
|
|
39
|
-
|
|
40
|
-
await Page.enable();
|
|
41
|
-
await Runtime.enable();
|
|
42
|
-
|
|
43
|
-
// Navigate
|
|
44
|
-
await Page.navigate({ url: `${this.#origin}${r}` });
|
|
45
|
-
await Page.loadEventFired(); // roughly equivalent to domcontentloaded+load
|
|
46
|
-
|
|
47
|
-
// Wait for Helmet (non-fatal)
|
|
48
|
-
await Runtime.evaluate({
|
|
49
|
-
expression: `
|
|
50
|
-
(async function () {
|
|
51
|
-
const deadline = Date.now() + 15000;
|
|
52
|
-
while (Date.now() < deadline) {
|
|
53
|
-
if (document.head && document.head.querySelector('[data-react-helmet="true"]')) return true;
|
|
54
|
-
await new Promise(r => setTimeout(r, 50));
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
})()
|
|
58
|
-
`,
|
|
59
|
-
awaitPromise: true,
|
|
60
|
-
returnByValue: true,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Small settle (like your 100ms)
|
|
64
|
-
await Runtime.evaluate({
|
|
65
|
-
expression: `new Promise(r => setTimeout(r, 100))`,
|
|
66
|
-
awaitPromise: true,
|
|
67
|
-
returnByValue: true,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Snapshot
|
|
71
|
-
const { result } = await Runtime.evaluate({
|
|
72
|
-
expression: "document.documentElement.outerHTML",
|
|
73
|
-
returnByValue: true,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return result.value;
|
|
77
|
-
} finally {
|
|
78
|
-
await client.close().catch(() => {});
|
|
79
|
-
await CDP.Close({ port: this.#cdpPort, id: target.id }).catch(() => {});
|
|
80
|
-
}
|
|
63
|
+
return result.value;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
async close() {
|
|
84
|
-
|
|
67
|
+
if (this.#client) {
|
|
68
|
+
await this.#client.close().catch(() => {});
|
|
69
|
+
this.#client = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
85
72
|
if (this.#chrome) {
|
|
86
73
|
try {
|
|
87
|
-
|
|
74
|
+
if (typeof this.#chrome.kill === "function") {
|
|
75
|
+
await this.#chrome.kill();
|
|
76
|
+
}
|
|
88
77
|
} catch {}
|
|
89
78
|
this.#chrome = null;
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
81
|
+
if (this.#chromeProcess) {
|
|
82
|
+
try {
|
|
83
|
+
this.#chromeProcess.kill("SIGKILL");
|
|
84
|
+
} catch {}
|
|
85
|
+
this.#chromeProcess = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
93
88
|
if (this.#server) {
|
|
94
89
|
await new Promise((res) => this.#server.close(res));
|
|
95
90
|
this.#server = null;
|
|
@@ -102,13 +97,118 @@ export class Prerenderer {
|
|
|
102
97
|
this.#indexPath = path.join(REMIXER_BUILD_DIR, "index.html");
|
|
103
98
|
}
|
|
104
99
|
|
|
100
|
+
async #verifyCDPReady() {
|
|
101
|
+
const maxAttempts = 20;
|
|
102
|
+
const delayMs = 500;
|
|
103
|
+
|
|
104
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
await CDP.List({ port: this.#cdpPort });
|
|
107
|
+
if (attempt > 1) {
|
|
108
|
+
console.log(`[ssg-helmet] CDP ready after ${attempt} attempts`);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.log(`[ssg-helmet] CDP verification attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
|
|
113
|
+
if (attempt === maxAttempts) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Chrome CDP not ready after ${maxAttempts * delayMs}ms. ` +
|
|
116
|
+
`Chrome may have crashed. Error: ${error.message}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
await setTimeout(delayMs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
async #init() {
|
|
106
125
|
await fs.access(this.#indexPath);
|
|
107
126
|
|
|
108
127
|
await this.#startServer();
|
|
109
128
|
|
|
129
|
+
// In Lambda (CHROME_BIN set), spawn Chrome directly to capture stderr.
|
|
130
|
+
// Locally, use chrome-launcher for convenience.
|
|
131
|
+
if (process.env.CHROME_BIN) {
|
|
132
|
+
await this.#launchDirect();
|
|
133
|
+
} else {
|
|
134
|
+
await this.#launchWithChromeLauncher();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`[ssg-helmet] Chrome process launched on port ${this.#cdpPort}, verifying CDP readiness...`);
|
|
138
|
+
await this.#verifyCDPReady();
|
|
139
|
+
console.log(`[ssg-helmet] Chrome CDP ready`);
|
|
140
|
+
|
|
141
|
+
// Connect to the default tab
|
|
142
|
+
const targets = await CDP.List({ port: this.#cdpPort });
|
|
143
|
+
const pageTarget = targets.find((t) => t.type === "page");
|
|
144
|
+
if (!pageTarget) throw new Error("No default page target found in Chrome");
|
|
145
|
+
|
|
146
|
+
this.#client = await CDP({ port: this.#cdpPort, target: pageTarget });
|
|
147
|
+
await this.#client.Page.enable();
|
|
148
|
+
await this.#client.Runtime.enable();
|
|
149
|
+
console.log(`[ssg-helmet] Connected to default tab`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async #launchDirect() {
|
|
153
|
+
const chromeBin = process.env.CHROME_BIN;
|
|
154
|
+
const debugPort = 9222 + Math.floor(Math.random() * 10000);
|
|
155
|
+
|
|
156
|
+
// Log Chrome version first
|
|
157
|
+
try {
|
|
158
|
+
const ver = spawn(chromeBin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
159
|
+
let out = "";
|
|
160
|
+
ver.stdout.on("data", (d) => (out += d));
|
|
161
|
+
await new Promise((res) => ver.on("close", res));
|
|
162
|
+
console.log(`[ssg-helmet] Chrome version: ${out.trim()}`);
|
|
163
|
+
} catch {}
|
|
164
|
+
|
|
165
|
+
const defaultFlags = [
|
|
166
|
+
"--headless",
|
|
167
|
+
"--no-sandbox",
|
|
168
|
+
"--disable-setuid-sandbox",
|
|
169
|
+
"--disable-gpu",
|
|
170
|
+
"--disable-dev-shm-usage",
|
|
171
|
+
"--no-zygote",
|
|
172
|
+
"--disable-background-networking",
|
|
173
|
+
"--disable-extensions",
|
|
174
|
+
"--disable-background-timer-throttling",
|
|
175
|
+
"--disable-backgrounding-occluded-windows",
|
|
176
|
+
"--disable-renderer-backgrounding",
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const chromeFlags = process.env.CHROME_FLAGS
|
|
180
|
+
? ["--headless=new", ...process.env.CHROME_FLAGS.split(",").map((f) => f.trim())]
|
|
181
|
+
: defaultFlags;
|
|
182
|
+
|
|
183
|
+
const allArgs = [
|
|
184
|
+
...chromeFlags,
|
|
185
|
+
`--remote-debugging-port=${debugPort}`,
|
|
186
|
+
"--remote-debugging-address=127.0.0.1",
|
|
187
|
+
"about:blank",
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
console.log(`[ssg-helmet] Spawning ${chromeBin} on port ${debugPort}`);
|
|
191
|
+
console.log(`[ssg-helmet] Args: ${allArgs.join(" ")}`);
|
|
192
|
+
|
|
193
|
+
this.#chromeProcess = spawn(chromeBin, allArgs, {
|
|
194
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let stderrBuf = "";
|
|
198
|
+
this.#chromeProcess.stderr.on("data", (d) => {
|
|
199
|
+
stderrBuf += d.toString();
|
|
200
|
+
});
|
|
201
|
+
this.#chromeProcess.on("exit", (code, signal) => {
|
|
202
|
+
console.log(`[ssg-helmet] Chrome exited: code=${code}, signal=${signal}`);
|
|
203
|
+
if (stderrBuf) console.log(`[ssg-helmet] Chrome stderr:\n${stderrBuf.slice(0, 4000)}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.#cdpPort = debugPort;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async #launchWithChromeLauncher() {
|
|
110
210
|
const defaultFlags = [
|
|
111
|
-
"--headless
|
|
211
|
+
"--headless",
|
|
112
212
|
"--no-sandbox",
|
|
113
213
|
"--disable-gpu",
|
|
114
214
|
"--disable-dev-shm-usage",
|
|
@@ -117,9 +217,11 @@ export class Prerenderer {
|
|
|
117
217
|
];
|
|
118
218
|
|
|
119
219
|
const chromeFlags = process.env.CHROME_FLAGS
|
|
120
|
-
? ["--headless
|
|
220
|
+
? ["--headless", ...process.env.CHROME_FLAGS.split(",").map((f) => f.trim())]
|
|
121
221
|
: defaultFlags;
|
|
122
222
|
|
|
223
|
+
console.log(`[ssg-helmet] Launching Chrome with flags:`, chromeFlags);
|
|
224
|
+
|
|
123
225
|
this.#chrome = await chromeLauncher.launch({
|
|
124
226
|
chromePath: process.env.CHROME_BIN || undefined,
|
|
125
227
|
chromeFlags,
|
|
@@ -198,6 +300,8 @@ export class Prerenderer {
|
|
|
198
300
|
#origin;
|
|
199
301
|
#indexPath;
|
|
200
302
|
|
|
303
|
+
#client;
|
|
201
304
|
#chrome;
|
|
305
|
+
#chromeProcess;
|
|
202
306
|
#cdpPort;
|
|
203
307
|
}
|