@three-blocks/devtools 11.0.0 → 11.1.0-alpha.395.g9b8ccb48a515
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/CHANGELOG.md +18 -0
- package/NOTICE +26 -0
- package/README.md +117 -170
- package/dist/asset-optimize-YJGWKD4Y.js +1 -0
- package/dist/asset-transforms-ZVZ4RD4L.js +11 -0
- package/dist/browser/shader-capture.js +2 -2
- package/dist/browser-smoke-4MYMHKWB.js +1 -0
- package/dist/chunk-4SVF5BRM.js +2 -0
- package/dist/chunk-7X4DHW36.js +1 -0
- package/dist/chunk-BBODS5RH.js +5 -0
- package/dist/{chunk-IILE5RBI.js → chunk-CMMYXGQ3.js} +1 -2
- package/dist/chunk-DCLDEPPB.js +253 -0
- package/dist/{chunk-5L2VS3BN.js → chunk-ECQE4D2U.js} +1 -2
- package/dist/chunk-IWQV456Z.js +3 -0
- package/dist/chunk-LSH64HEU.js +80 -0
- package/dist/{chunk-X5OFYHIY.js → chunk-MIJXPQPM.js} +2 -3
- package/dist/chunk-NDIPQLTU.js +16 -0
- package/dist/{chunk-NPLE7PPF.js → chunk-NJITVI23.js} +1 -2
- package/dist/chunk-O5FU2ASS.js +16 -0
- package/dist/chunk-QQKHDH26.js +1 -0
- package/dist/chunk-S5LI2C2H.js +5 -0
- package/dist/chunk-SBUKHO24.js +1 -0
- package/dist/chunk-TK566QET.js +1 -0
- package/dist/{chunk-65VEJRRT.js → chunk-WNSGAV6N.js} +1 -2
- package/dist/chunk-ZG3LO3VB.js +1 -0
- package/dist/{chunk-UDUICSME.js → chunk-ZTHZE36A.js} +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -26
- package/dist/environment-bake-ON5I27N7.js +1 -0
- package/dist/index.d.ts +24 -12
- package/dist/index.js +1 -279
- package/dist/{shader-autowire-SPE5DM2X.js → shader-autowire-D2EOG6GF.js} +1 -2
- package/dist/shader-browser-6ICBHK3S.js +1 -0
- package/dist/shader-capture-server.d.ts +1 -1
- package/dist/shader-capture-server.js +1 -1
- package/dist/{shader-config-EOFU5D3Y.js → shader-config-AKZEKIRB.js} +1 -2
- package/dist/{shader-parity-QXJWR6OZ.js → shader-parity-7DEPQEDI.js} +1 -1
- package/dist/{shader-project-runner-S2AID2RJ.js → shader-project-runner-NV4GKHDX.js} +1 -2
- package/dist/shader-watch-JBUA4LP5.js +1 -0
- package/dist/shader-workflow-5EHHR32D.js +1 -0
- package/dist/status-GDVSLQWV.js +1 -0
- package/dist/text-atlas-YYWV7S6F.js +1 -0
- package/dist/transform.cjs +1 -1
- package/dist/transform.js +1 -1
- package/dist/webpack-loader.cjs +1 -1
- package/dist/webpack-loader.js +1 -1
- package/node/index.d.mts +70 -0
- package/node/index.mjs +407 -0
- package/node/ipc.mjs +31 -0
- package/node/layout.mjs +546 -0
- package/node/loader.mjs +103 -0
- package/node/media.mjs +454 -0
- package/node/recording.mjs +67 -0
- package/node/worker.mjs +1314 -0
- package/package.json +27 -11
- package/dist/asset-optimize-GG72JCOX.js +0 -6
- package/dist/browser-smoke-KFF7XV7F.js +0 -254
- package/dist/chunk-45AVKQQK.js +0 -80
- package/dist/chunk-5FT6AZD6.js +0 -82
- package/dist/chunk-7N6TU4JG.js +0 -17
- package/dist/chunk-EI7MMDWY.js +0 -1
- package/dist/chunk-HOZDD6PW.js +0 -2
- package/dist/chunk-LKXY5YU2.js +0 -17
- package/dist/chunk-MIUIIPS4.js +0 -1
- package/dist/chunk-NF4RFSUJ.js +0 -18
- package/dist/chunk-QJFPNM6W.js +0 -2
- package/dist/chunk-ZT6T2W4B.js +0 -2
- package/dist/chunk-ZZLAZ7GJ.js +0 -6
- package/dist/environment-bake-BGMGFAVK.js +0 -4
- package/dist/shader-browser-H5CB2ZQX.js +0 -2
- package/dist/shader-browser-UXXR6QI2.js +0 -1
- package/dist/shader-parity-XPKQQK26.js +0 -2
- package/dist/shader-watch-EORZ2SEC.js +0 -2
- package/dist/shader-workflow-7NQFDQI5.js +0 -2
- package/dist/status-PTBB4SW2.js +0 -2
- package/dist/text-atlas-UORTLOQJ.js +0 -2
- /package/dist/{chunk-TRHSN2DJ.cjs → chunk-LMLJIVNY.cjs} +0 -0
package/node/index.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { writeFile } from 'node:fs/promises';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { encodeIPC, decodeIPC } from './ipc.mjs';
|
|
8
|
+
|
|
9
|
+
const jsonIPC = Boolean( process.versions.bun );
|
|
10
|
+
|
|
11
|
+
// The small page API shared by our GPU harnesses and shader driver. Each page
|
|
12
|
+
// owns a Node process: native crashes, module caches and GPU lifetimes stay local.
|
|
13
|
+
class NodePage extends EventEmitter {
|
|
14
|
+
|
|
15
|
+
#context;
|
|
16
|
+
#child;
|
|
17
|
+
#pending = new Map();
|
|
18
|
+
#sequence = 0;
|
|
19
|
+
#failure;
|
|
20
|
+
#closing = false;
|
|
21
|
+
#url;
|
|
22
|
+
#ready = false;
|
|
23
|
+
#requests = new Map();
|
|
24
|
+
#recording;
|
|
25
|
+
#frame = { url: () => this.#url, parentFrame: () => null };
|
|
26
|
+
|
|
27
|
+
constructor( context ) {
|
|
28
|
+
|
|
29
|
+
super();
|
|
30
|
+
this.#context = context;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
context() { return this.#context; }
|
|
35
|
+
mainFrame() { return this.#frame; }
|
|
36
|
+
url() { return this.#url; }
|
|
37
|
+
isClosed() { return this.#closing; }
|
|
38
|
+
route( pattern, handler ) { return this.#context.route( pattern, handler ); }
|
|
39
|
+
|
|
40
|
+
async goto( url, { timeout = 30_000 } = {} ) {
|
|
41
|
+
url = new URL( url ).href;
|
|
42
|
+
|
|
43
|
+
if ( this.#child ) await this.close();
|
|
44
|
+
this.#closing = false;
|
|
45
|
+
this.#url = String( url );
|
|
46
|
+
this.#ready = false;
|
|
47
|
+
this.#requests.clear();
|
|
48
|
+
this.#failure = undefined;
|
|
49
|
+
this.#child = fork( new URL( './worker.mjs', import.meta.url ), [], {
|
|
50
|
+
execPath: process.versions.bun ? 'node' : process.execPath,
|
|
51
|
+
execArgv: [ '--experimental-vm-modules', '--expose-gc' ], serialization: jsonIPC ? 'json' : 'advanced', stdio: [ 'ignore', 'pipe', 'pipe', 'ipc' ],
|
|
52
|
+
env: { ...process.env, THREE_BLOCKS_NODE_JSON_IPC: jsonIPC ? '1' : '0' },
|
|
53
|
+
} );
|
|
54
|
+
let output = '';
|
|
55
|
+
for ( const stream of [ this.#child.stdout, this.#child.stderr ] ) stream.on( 'data', data => {
|
|
56
|
+
|
|
57
|
+
output = ( output + data ).slice( -32_768 );
|
|
58
|
+
|
|
59
|
+
} );
|
|
60
|
+
this.#child.on( 'message', async message => {
|
|
61
|
+
|
|
62
|
+
if ( jsonIPC ) message = decodeIPC( message );
|
|
63
|
+
if ( message.event === 'console' ) this.emit( 'console', {
|
|
64
|
+
type: () => message.level, text: () => message.text, location: () => ( { url } ),
|
|
65
|
+
} );
|
|
66
|
+
else if ( message.event === 'network' ) this.#network( message );
|
|
67
|
+
else if ( message.event === 'storage' ) this.#context.storage = message.storage;
|
|
68
|
+
else if ( message.event === 'cookies' ) this.#context.cookies = message.cookies;
|
|
69
|
+
else if ( message.event === 'url' ) this.#url = message.url;
|
|
70
|
+
else if ( message.event === 'unhandledrejection' ) { this.emit( 'unhandledrejection', message.error ); this.#fail( new Error( message.error ) ); }
|
|
71
|
+
else if ( message.event === 'navigate' ) void this.goto( message.url ).catch( error => this.#fail( error ) );
|
|
72
|
+
else if ( message.event === 'pageerror' ) this.#fail( new Error( message.error ) );
|
|
73
|
+
else if ( message.event === 'route' ) {
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
|
|
77
|
+
let response;
|
|
78
|
+
await this.#context.handler?.( {
|
|
79
|
+
request: () => ( {
|
|
80
|
+
url: () => message.url, method: () => message.method,
|
|
81
|
+
headers: () => message.headers, resourceType: () => message.resourceType,
|
|
82
|
+
postDataBuffer: () => message.body === undefined ? null : Buffer.from( message.body ),
|
|
83
|
+
} ),
|
|
84
|
+
fulfill: async value => { response = value; },
|
|
85
|
+
abort: async ( reason = 'blockedbyclient' ) => { response = { error: reason }; },
|
|
86
|
+
continue: async () => {},
|
|
87
|
+
} );
|
|
88
|
+
const reply = { route: message.id, response };
|
|
89
|
+
this.#child?.send( jsonIPC ? encodeIPC( reply ) : reply );
|
|
90
|
+
|
|
91
|
+
} catch ( error ) {
|
|
92
|
+
const reply = { route: message.id, response: { error: String( error ) } };
|
|
93
|
+
this.#child?.send( jsonIPC ? encodeIPC( reply ) : reply );
|
|
94
|
+
this.#fail( error );
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
} else {
|
|
98
|
+
|
|
99
|
+
const pending = this.#pending.get( message.id );
|
|
100
|
+
if ( ! pending ) return;
|
|
101
|
+
this.#pending.delete( message.id );
|
|
102
|
+
clearTimeout( pending.timer );
|
|
103
|
+
if ( message.error ) pending.reject( new Error( message.error ) );
|
|
104
|
+
else pending.resolve( message.value );
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} );
|
|
109
|
+
this.#child.on( 'error', error => this.#fail( error ) );
|
|
110
|
+
this.#child.on( 'exit', ( code, signal ) => {
|
|
111
|
+
|
|
112
|
+
if ( ! this.#closing ) this.#fail( new Error( `Native renderer exited (${signal ?? code}). ${output}` ) );
|
|
113
|
+
|
|
114
|
+
} );
|
|
115
|
+
const result = await this.#call( 'goto', {
|
|
116
|
+
url, viewport: this.#context.viewport, scripts: this.#context.scripts,
|
|
117
|
+
routes: Boolean( this.#context.handler ), storage: this.#context.storage, media: this.#context.media,
|
|
118
|
+
cookies: this.#context.cookies, gpu: this.#context.gpu,
|
|
119
|
+
}, timeout );
|
|
120
|
+
if ( this.#context.recordVideo ) this.#recording = await this.screencast( { path: join( this.#context.recordVideo.dir, `${randomUUID()}.webm` ) } );
|
|
121
|
+
this.#ready = true;
|
|
122
|
+
this.emit( 'framenavigated', this.#frame );
|
|
123
|
+
return result;
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#network( message ) {
|
|
128
|
+
|
|
129
|
+
if ( message.phase === 'request' ) {
|
|
130
|
+
|
|
131
|
+
const row = { ...message };
|
|
132
|
+
row.request = { url: () => row.url, method: () => row.method, headers: () => row.headers, resourceType: () => row.resourceType ?? 'fetch', failure: () => row.error ? { errorText: row.error } : null, response: async () => row.response };
|
|
133
|
+
this.#requests.set( message.id, row ); this.emit( 'request', row.request );
|
|
134
|
+
|
|
135
|
+
} else {
|
|
136
|
+
|
|
137
|
+
const row = this.#requests.get( message.id );
|
|
138
|
+
if ( ! row ) return;
|
|
139
|
+
if ( message.phase === 'response' ) {
|
|
140
|
+
|
|
141
|
+
row.response = { url: () => row.url, request: () => row.request, status: () => message.status, statusText: () => message.statusText, headers: () => message.headers, body: async () => row.body ?? Buffer.alloc( 0 ) };
|
|
142
|
+
this.emit( 'response', row.response );
|
|
143
|
+
|
|
144
|
+
} else {
|
|
145
|
+
|
|
146
|
+
row.body = message.body === undefined ? undefined : Buffer.from( message.body ); row.error = message.error;
|
|
147
|
+
this.emit( message.phase === 'failed' ? 'requestfailed' : 'requestfinished', row.request );
|
|
148
|
+
this.#requests.delete( message.id );
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#fail( error ) {
|
|
157
|
+
|
|
158
|
+
this.#failure ??= error;
|
|
159
|
+
for ( const pending of this.#pending.values() ) {
|
|
160
|
+
|
|
161
|
+
clearTimeout( pending.timer ); pending.reject( error );
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
this.#pending.clear();
|
|
165
|
+
this.emit( 'pageerror', error );
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#call( method, args, timeout = 240_000 ) {
|
|
170
|
+
|
|
171
|
+
if ( this.#failure ) return Promise.reject( this.#failure );
|
|
172
|
+
if ( ! this.#child?.connected ) return Promise.reject( new Error( 'Native renderer is not running.' ) );
|
|
173
|
+
const id = ++ this.#sequence;
|
|
174
|
+
return new Promise( ( resolve, reject ) => {
|
|
175
|
+
|
|
176
|
+
const timer = setTimeout( () => {
|
|
177
|
+
|
|
178
|
+
this.#pending.delete( id );
|
|
179
|
+
reject( new Error( `Native renderer ${method} timed out after ${timeout}ms.` ) );
|
|
180
|
+
void this.close();
|
|
181
|
+
|
|
182
|
+
}, timeout );
|
|
183
|
+
this.#pending.set( id, { resolve, reject, timer } );
|
|
184
|
+
const message = { id, method, args };
|
|
185
|
+
this.#child.send( jsonIPC ? encodeIPC( message ) : message, error => { if ( error ) this.#fail( error ); } );
|
|
186
|
+
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
evaluate( fn, ...args ) { return this.#call( 'evaluate', { source: String( fn ), args, callable: typeof fn === 'function' } ); }
|
|
192
|
+
|
|
193
|
+
async evaluateHandle( fn, ...args ) {
|
|
194
|
+
|
|
195
|
+
const { id, element } = await this.#call( 'handle', { source: String( fn ), args, callable: typeof fn === 'function' } );
|
|
196
|
+
const handle = {
|
|
197
|
+
asElement: () => element ? handle : null,
|
|
198
|
+
evaluate: ( fn, ...args ) => this.#call( 'handleEvaluate', { id, source: String( fn ), args } ),
|
|
199
|
+
boundingBox: () => handle.evaluate( element => { const { x, y, width, height } = element.getBoundingClientRect(); return { x, y, width, height }; } ),
|
|
200
|
+
screenshot: options => this.screenshot( { ...options, handle: id } ),
|
|
201
|
+
dispose: () => this.#call( 'releaseHandle', { id } ),
|
|
202
|
+
};
|
|
203
|
+
return handle;
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async $( selector ) { return ( await this.evaluate( selector => document.querySelector( selector ) !== null, selector ) ) ? ( await this.evaluateHandle( selector => document.querySelector( selector ), selector ) ).asElement() : null; }
|
|
208
|
+
|
|
209
|
+
async waitForSelector( selector, options ) {
|
|
210
|
+
|
|
211
|
+
await this.waitForFunction( selector => document.querySelector( selector ) !== null, selector, options );
|
|
212
|
+
return this.evaluateHandle( selector => document.querySelector( selector ), selector );
|
|
213
|
+
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async evaluateOnNewDocument( fn, ...args ) { await this.#context.addInitScript( `(${fn})(...${JSON.stringify( args )});` ); }
|
|
217
|
+
viewport() { return this.#context.viewport; }
|
|
218
|
+
async setViewport( viewport ) {
|
|
219
|
+
|
|
220
|
+
Object.assign( this.#context.viewport, viewport );
|
|
221
|
+
if ( this.#child ) await this.evaluate( viewport => { Object.assign( globalThis, { innerWidth: viewport.width, innerHeight: viewport.height, devicePixelRatio: viewport.deviceScaleFactor ?? 1 } ); dispatchEvent( new Event( 'resize' ) ); }, viewport );
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
async emulateMediaFeatures( features ) { this.#context.media = features; }
|
|
225
|
+
async bringToFront() {}
|
|
226
|
+
async waitForLoadState() {} // goto resolves after the document's scripts and load event.
|
|
227
|
+
reload( options ) { return this.goto( this.#url, options ); }
|
|
228
|
+
mouse = {
|
|
229
|
+
move: ( x, y ) => this.evaluate( ( { x, y } ) => {
|
|
230
|
+
const target = document.querySelector( 'canvas' ) ?? document.body;
|
|
231
|
+
for ( const type of [ 'pointermove', 'mousemove' ] ) target.dispatchEvent( new PointerEvent( type, { clientX: x, clientY: y, bubbles: true, pointerId: 1, pointerType: 'mouse' } ) );
|
|
232
|
+
}, { x, y } ),
|
|
233
|
+
wheel: options => this.evaluate( options => ( document.querySelector( 'canvas' ) ?? document.body ).dispatchEvent( new WheelEvent( 'wheel', { ...options, bubbles: true } ) ), options ),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
async waitForFunction( fn, arg, options ) {
|
|
237
|
+
|
|
238
|
+
if ( options === undefined && arg && ( 'timeout' in Object( arg ) || 'polling' in Object( arg ) ) ) { options = arg; arg = undefined; }
|
|
239
|
+
const { timeout = 30_000 } = options ?? {};
|
|
240
|
+
|
|
241
|
+
return this.#call( 'wait', { source: String( fn ), arg, callable: typeof fn === 'function', timeout }, timeout + 1_000 );
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async screenshot( options = {} ) {
|
|
246
|
+
|
|
247
|
+
const bytes = Buffer.from( await this.#call( 'screenshot', { selector: options.selector, index: options.index, handle: options.handle, clip: options.clip, omitBackground: options.omitBackground }, options.timeout ) );
|
|
248
|
+
if ( options.path ) await writeFile( options.path, bytes );
|
|
249
|
+
return options.encoding === 'base64' ? bytes.toString( 'base64' ) : bytes;
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
locator( selector ) {
|
|
254
|
+
|
|
255
|
+
const make = steps => {
|
|
256
|
+
|
|
257
|
+
const evaluateAll = ( fn, arg ) => this.evaluate( ( { steps, source, arg } ) => ( 0, eval )( `(${source})` )( globalThis.__THREE_BLOCKS_LOCATE__( steps ), arg ), { steps, source: String( fn ), arg } );
|
|
258
|
+
const evaluate = ( fn, arg ) => evaluateAll( ( elements, { source, arg } ) => { if ( ! elements[ 0 ] ) throw new Error( 'Native locator matched no element.' ); return ( 0, eval )( `(${source})` )( elements[ 0 ], arg ); }, { source: String( fn ), arg } );
|
|
259
|
+
const locator = {
|
|
260
|
+
locator: selector => make( [ ...steps, { selector } ] ),
|
|
261
|
+
nth: index => make( [ ...steps.slice( 0, -1 ), { ...steps.at( -1 ), index } ] ),
|
|
262
|
+
first: () => locator.nth( 0 ),
|
|
263
|
+
filter: filter => make( [ ...steps.slice( 0, -1 ), { ...steps.at( -1 ), text: filter.hasText } ] ),
|
|
264
|
+
evaluate, evaluateAll,
|
|
265
|
+
count: () => evaluateAll( elements => elements.length ),
|
|
266
|
+
textContent: () => evaluate( element => element.textContent ),
|
|
267
|
+
innerHTML: () => evaluate( element => element.innerHTML ),
|
|
268
|
+
getAttribute: name => evaluate( ( element, name ) => element.getAttribute( name ), name ),
|
|
269
|
+
isVisible: () => evaluateAll( elements => Boolean( elements[ 0 ] && ! elements[ 0 ].hidden && getComputedStyle( elements[ 0 ] ).display !== 'none' ) ),
|
|
270
|
+
boundingBox: () => evaluate( element => { const { x, y, width, height } = element.getBoundingClientRect(); return { x, y, width, height }; } ),
|
|
271
|
+
screenshot: async options => {
|
|
272
|
+
|
|
273
|
+
const handle = await this.evaluateHandle( steps => globalThis.__THREE_BLOCKS_LOCATE__( steps )[ 0 ], steps );
|
|
274
|
+
try { return await handle.screenshot( options ); } finally { await handle.dispose(); }
|
|
275
|
+
|
|
276
|
+
},
|
|
277
|
+
click: () => evaluate( element => { element.focus(); element.click(); if ( element.tagName === 'SUMMARY' ) element.parentElement.open = ! element.parentElement.open; } ),
|
|
278
|
+
hover: () => evaluate( element => { for ( const type of [ 'pointerover', 'mouseover', 'mouseenter' ] ) element.dispatchEvent( new MouseEvent( type, { bubbles: true } ) ); } ),
|
|
279
|
+
focus: () => evaluate( element => element.focus() ),
|
|
280
|
+
};
|
|
281
|
+
return locator;
|
|
282
|
+
|
|
283
|
+
};
|
|
284
|
+
return make( [ { selector } ] );
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
keyboard = { press: key => this.evaluate( key => dispatchEvent( Object.assign( new Event( 'keydown' ), { key, code: key, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false } ) ), key ) };
|
|
289
|
+
async waitForURL( expected, { timeout = 30_000 } = {} ) {
|
|
290
|
+
|
|
291
|
+
const deadline = performance.now() + timeout;
|
|
292
|
+
while ( ! this.#ready || ! ( typeof expected === 'function' ? expected( new URL( this.#url ) ) : expected instanceof RegExp ? expected.test( this.#url ) : this.#url === expected ) ) {
|
|
293
|
+
|
|
294
|
+
if ( this.#failure ) throw this.#failure;
|
|
295
|
+
if ( performance.now() >= deadline ) throw new Error( `Native navigation did not reach ${expected}.` );
|
|
296
|
+
await delay( 5 );
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
waitForTimeout( ms ) { return delay( ms ); }
|
|
303
|
+
|
|
304
|
+
async screencast( options ) {
|
|
305
|
+
|
|
306
|
+
const { recordCanvas } = await import( './recording.mjs' );
|
|
307
|
+
return recordCanvas( () => this.#call( 'pixels', {} ), options );
|
|
308
|
+
|
|
309
|
+
}
|
|
310
|
+
video() { return this.#recording ?? null; }
|
|
311
|
+
|
|
312
|
+
async close() {
|
|
313
|
+
|
|
314
|
+
let recordingError;
|
|
315
|
+
try { await this.#recording?.stop(); } catch ( error ) { recordingError = error; }
|
|
316
|
+
this.#closing = true;
|
|
317
|
+
const child = this.#child;
|
|
318
|
+
this.#child = undefined;
|
|
319
|
+
if ( child && child.exitCode === null && child.signalCode === null ) await new Promise( resolve => {
|
|
320
|
+
|
|
321
|
+
child.once( 'exit', resolve ); child.kill( 'SIGKILL' );
|
|
322
|
+
|
|
323
|
+
} );
|
|
324
|
+
for ( const pending of this.#pending.values() ) {
|
|
325
|
+
|
|
326
|
+
clearTimeout( pending.timer ); pending.reject( new Error( 'Native renderer closed.' ) );
|
|
327
|
+
|
|
328
|
+
}
|
|
329
|
+
this.#pending.clear();
|
|
330
|
+
if ( recordingError ) throw recordingError;
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class NodeContext {
|
|
337
|
+
|
|
338
|
+
scripts = [];
|
|
339
|
+
pages = [];
|
|
340
|
+
storage = {};
|
|
341
|
+
cookies;
|
|
342
|
+
media = [];
|
|
343
|
+
constructor( browser, { viewport = { width: 1280, height: 720 }, deviceScaleFactor = 1, frameIntervalMs = 0, recordVideo, gpu = true } = {} ) {
|
|
344
|
+
|
|
345
|
+
if ( ! Number.isFinite( frameIntervalMs ) || frameIntervalMs < 0 ) throw new TypeError( 'frameIntervalMs must be a non-negative finite number.' );
|
|
346
|
+
this.owner = browser; this.viewport = { ...viewport, deviceScaleFactor, frameIntervalMs }; this.recordVideo = recordVideo;
|
|
347
|
+
this.gpu = gpu;
|
|
348
|
+
|
|
349
|
+
}
|
|
350
|
+
browser() { return this.owner; }
|
|
351
|
+
async newPage() { const page = new NodePage( this ); this.pages.push( page ); return page; }
|
|
352
|
+
async addInitScript( script, arg ) { this.scripts.push( typeof script === 'function' ? `(${script})(${JSON.stringify( arg )});` : typeof script === 'string' ? script : script.content ); }
|
|
353
|
+
async route( pattern, handler ) {
|
|
354
|
+
|
|
355
|
+
if ( this.pages.some( page => page.url() && ! page.isClosed() ) ) throw new Error( 'Configure native request routes before navigation.' );
|
|
356
|
+
const previous = this.handler;
|
|
357
|
+
const regex = new RegExp( '^' + pattern.replace( /[.+?^${}()|[\]\\]/g, '\\$&' ).replace( /\*\*/g, '\u0000' ).replace( /\*/g, '[^/]*' ).replace( /\u0000/g, '.*' ) + '$' );
|
|
358
|
+
this.handler = route => regex.test( route.request().url() ) ? handler( route ) : previous?.( route );
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
async close() { await Promise.all( this.pages.map( page => page.close() ) ); this.pages = []; }
|
|
362
|
+
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function launchNodeRenderer() {
|
|
366
|
+
|
|
367
|
+
const renderer = new EventEmitter();
|
|
368
|
+
renderer.execution = 'node';
|
|
369
|
+
renderer.gpuInfo = async () => {
|
|
370
|
+
|
|
371
|
+
const context = await renderer.newContext();
|
|
372
|
+
try {
|
|
373
|
+
|
|
374
|
+
const page = await context.newPage();
|
|
375
|
+
await page.goto( 'data:text/html,<html><body></body></html>' );
|
|
376
|
+
return await page.evaluate( async () => {
|
|
377
|
+
|
|
378
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
379
|
+
return { execution: 'node', ...Object.fromEntries( [ 'vendor', 'architecture', 'device', 'description', 'isFallbackAdapter' ].map( key => [ key, adapter.info[ key ] ] ) ) };
|
|
380
|
+
|
|
381
|
+
} );
|
|
382
|
+
|
|
383
|
+
} finally { await context.close(); }
|
|
384
|
+
|
|
385
|
+
};
|
|
386
|
+
const contexts = [];
|
|
387
|
+
let connected = true;
|
|
388
|
+
renderer.isConnected = () => connected;
|
|
389
|
+
renderer.version = () => `Node ${process.versions.node} (ANGLE/Dawn)`;
|
|
390
|
+
renderer.newContext = async options => {
|
|
391
|
+
|
|
392
|
+
const context = new NodeContext( renderer, options ); contexts.push( context ); return context;
|
|
393
|
+
|
|
394
|
+
};
|
|
395
|
+
renderer.newPage = async options => ( await renderer.newContext( options ) ).newPage();
|
|
396
|
+
renderer.close = async () => {
|
|
397
|
+
|
|
398
|
+
await Promise.all( contexts.map( context => context.close() ) );
|
|
399
|
+
contexts.length = 0; connected = false; renderer.emit( 'disconnected' );
|
|
400
|
+
process.off( 'exit', cleanup );
|
|
401
|
+
|
|
402
|
+
};
|
|
403
|
+
const cleanup = () => { void renderer.close(); };
|
|
404
|
+
process.once( 'exit', cleanup );
|
|
405
|
+
return renderer;
|
|
406
|
+
|
|
407
|
+
}
|
package/node/ipc.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { serialize, deserialize } from '@ungap/structured-clone';
|
|
2
|
+
|
|
3
|
+
// Bun and Node have incompatible binary IPC formats. Keep Node's fast native
|
|
4
|
+
// transport; Bun uses the existing structured-clone codec over JSON instead.
|
|
5
|
+
export function encodeIPC( value ) {
|
|
6
|
+
|
|
7
|
+
return JSON.stringify( serialize( value ), ( _key, item ) => {
|
|
8
|
+
|
|
9
|
+
if ( typeof item === 'bigint' ) return { bigint: item.toString() };
|
|
10
|
+
if ( typeof item === 'number' && ( ! Number.isFinite( item ) || Object.is( item, -0 ) ) ) return { number: Object.is( item, -0 ) ? '-0' : String( item ) };
|
|
11
|
+
return item;
|
|
12
|
+
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decodeIPC( value ) {
|
|
18
|
+
|
|
19
|
+
return deserialize( JSON.parse( value, ( _key, item ) => {
|
|
20
|
+
|
|
21
|
+
if ( item && typeof item === 'object' && ! Array.isArray( item ) ) {
|
|
22
|
+
|
|
23
|
+
if ( typeof item.number === 'string' ) return Number( item.number );
|
|
24
|
+
if ( typeof item.bigint === 'string' ) return BigInt( item.bigint );
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
return item;
|
|
28
|
+
|
|
29
|
+
} ) );
|
|
30
|
+
|
|
31
|
+
}
|