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-d888b2d",
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
- const prerenderer = await Prerenderer.create();
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
- // Create a fresh tab (CDP target) for isolation + parallelism
22
- // Retry connection to allow CDP server to initialize
23
- let target, client;
24
- for (let attempt = 1; attempt <= 10; attempt++) {
25
- try {
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
- const { Page, Runtime } = client;
57
+ // Snapshot
58
+ const { result } = await Runtime.evaluate({
59
+ expression: "document.documentElement.outerHTML",
60
+ returnByValue: true,
61
+ });
38
62
 
39
- try {
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
- // stop chromium
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
- await this.#chrome.kill();
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
- // stop server
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=new",
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=new", ...process.env.CHROME_FLAGS.split(",").map(f => f.trim())]
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
  }