11import fs from "node:fs" ;
22import path from "node:path" ;
3+ import process from "node:process" ;
34import { spawn } from "node:child_process" ;
5+ import * as puppeteer from "puppeteer-core" ;
46
57const OUTPUT_BASE = path . join ( "./public" , "whitepaper" , "pdf" ) ;
68const PORT = 4322 ;
79const BASE_URL = `http://host.docker.internal:${ PORT } ` ;
810
11+ const BROWSERLESS_WS = process . env . BROWSERLESS_WS ?? "ws://127.0.0.1:33000" ;
12+
913const SUPPORTED_LOCALES = [
1014 "en-US" ,
1115 "zh-CN" ,
@@ -19,6 +23,55 @@ const SUPPORTED_LOCALES = [
1923] ;
2024const DEFAULT_LOCALE = "en-US" ;
2125
26+ /** Time with no network activity before considering the page idle (ms). */
27+ const NETWORK_IDLE_MS = 500 ;
28+ /** Max time to wait for network idle after scrolling (ms). */
29+ const NETWORK_IDLE_TIMEOUT_MS = 90_000 ;
30+ /** Pause between scroll steps so lazy observers can fire (ms). */
31+ const SCROLL_STEP_PAUSE_MS = 75 ;
32+
33+ /**
34+ * Scrolls the full document to trigger lazy-loaded images, waits for network
35+ * quiet, then resolves when images have loaded (or failed).
36+ */
37+ async function waitForImagesAndNetworkIdle ( page ) {
38+ await page . evaluate ( async ( stepPause ) => {
39+ const sleep = ( ms ) => new Promise ( ( r ) => setTimeout ( r , ms ) ) ;
40+ const step = Math . max ( 240 , Math . floor ( window . innerHeight * 0.9 ) ) ;
41+ const maxY = document . documentElement . scrollHeight ;
42+ for ( let y = 0 ; y < maxY ; y += step ) {
43+ window . scrollTo ( 0 , y ) ;
44+ await sleep ( stepPause ) ;
45+ }
46+ window . scrollTo ( 0 , maxY ) ;
47+ await sleep ( stepPause ) ;
48+ window . scrollTo ( 0 , 0 ) ;
49+ await sleep ( 100 ) ;
50+ } , SCROLL_STEP_PAUSE_MS ) ;
51+
52+ await page . waitForNetworkIdle ( {
53+ idleTime : NETWORK_IDLE_MS ,
54+ timeout : NETWORK_IDLE_TIMEOUT_MS ,
55+ } ) ;
56+
57+ await page . evaluate ( async ( ) => {
58+ const imgs = Array . from ( document . images ) ;
59+ await Promise . all (
60+ imgs . map ( ( img ) => {
61+ if ( img . complete && img . naturalWidth > 0 ) {
62+ return img . decode ?. ( ) . catch ( ( ) => { } ) ?? Promise . resolve ( ) ;
63+ }
64+ return new Promise ( ( resolve ) => {
65+ const done = ( ) => resolve ( ) ;
66+ img . addEventListener ( "load" , done , { once : true } ) ;
67+ img . addEventListener ( "error" , done , { once : true } ) ;
68+ setTimeout ( done , 15_000 ) ;
69+ } ) . then ( ( ) => img . decode ?. ( ) . catch ( ( ) => { } ) ?? Promise . resolve ( ) ) ;
70+ } ) ,
71+ ) ;
72+ } ) ;
73+ }
74+
2275function discoverVersions ( ) {
2376 const contentDir = "./src/contents/whitepapers" ;
2477 const versions = new Map ( ) ;
@@ -98,25 +151,20 @@ async function generatePdf(puppeteer, locale, version) {
98151
99152 console . log ( ` Generating PDF: ${ locale } /v${ version } -> ${ outputFile } ` ) ;
100153
101- let browser ;
102- try {
103- browser = await puppeteer . connect ( {
104- browserWSEndpoint : `ws://127.0.0.1:33000` ,
105- } ) ;
106- } catch ( err ) {
107- console . warn ( `\nCould not connect to browser: ${ err . message } ` ) ;
108- console . log ( "Skipping PDF generation." ) ;
109- return ;
110- }
154+ const browser = await puppeteer . connect ( {
155+ browserWSEndpoint : BROWSERLESS_WS ,
156+ } ) ;
111157
112158 const page = await browser . newPage ( ) ;
113159
114160 try {
115161 await page . goto ( url , {
116162 waitUntil : "networkidle0" ,
117- timeout : 30000 ,
163+ timeout : 60_000 ,
118164 } ) ;
119165
166+ await waitForImagesAndNetworkIdle ( page ) ;
167+
120168 await page . pdf ( {
121169 path : outputFile ,
122170 format : "A4" ,
@@ -153,11 +201,11 @@ async function generatePdf(puppeteer, locale, version) {
153201 ` Warning: Failed to generate PDF for ${ locale } /v${ version } : ${ err . message } ` ,
154202 ) ;
155203 } finally {
156- await browser . close ( ) ;
204+ await browser ? .close ( ) ;
157205 }
158206}
159207
160- async function main ( ) {
208+ export async function main ( ) {
161209 console . log ( "Whitepaper PDF Generation" ) ;
162210 console . log ( "=" . repeat ( 50 ) ) ;
163211
@@ -172,17 +220,6 @@ async function main() {
172220 console . log ( ` ${ locale } : ${ versions . map ( ( v ) => `v${ v } ` ) . join ( ", " ) } ` ) ;
173221 }
174222
175- let puppeteer ;
176- try {
177- puppeteer = await import ( "puppeteer" ) ;
178- } catch {
179- console . warn ( "\nPuppeteer not available. Skipping PDF generation." ) ;
180- console . log (
181- "To enable PDF generation, run: npx puppeteer browsers install chrome" ,
182- ) ;
183- return ;
184- }
185-
186223 console . log ( "\nStarting preview server..." ) ;
187224 const server = await startPreviewServer ( ) ;
188225
@@ -198,10 +235,7 @@ async function main() {
198235 console . log ( "\nAll PDFs generated successfully." ) ;
199236 } catch ( err ) {
200237 console . error ( "PDF generation failed:" , err . message ) ;
201- console . log ( "Build completed but PDF generation was skipped." ) ;
202238 }
203239
204240 server . kill ( ) ;
205241}
206-
207- main ( ) ;
0 commit comments