extension-develop 3.17.0 → 3.18.0

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.
Files changed (43) hide show
  1. package/dist/0~dev-server.mjs +660 -59
  2. package/dist/0~rspack-config.mjs +867 -881
  3. package/dist/0~zip.mjs +39 -10
  4. package/dist/45.mjs +39 -0
  5. package/dist/946.mjs +142 -36
  6. package/dist/962.mjs +299 -250
  7. package/dist/bridge.mjs +228 -0
  8. package/dist/extension-js-devtools/chrome/content_scripts/content-0.js +2 -2
  9. package/dist/extension-js-devtools/chrome/pages/centralized-logger.js +1 -1
  10. package/dist/extension-js-devtools/chrome/pages/welcome.js +2 -2
  11. package/dist/extension-js-devtools/chrome/scripts/logger-client.js +1 -1
  12. package/dist/extension-js-devtools/chromium/content_scripts/content-0.js +2 -2
  13. package/dist/extension-js-devtools/chromium/pages/centralized-logger.js +1 -1
  14. package/dist/extension-js-devtools/chromium/pages/welcome.js +2 -2
  15. package/dist/extension-js-devtools/chromium/scripts/logger-client.js +1 -1
  16. package/dist/extension-js-devtools/edge/content_scripts/content-0.js +2 -2
  17. package/dist/extension-js-devtools/edge/pages/centralized-logger.js +1 -1
  18. package/dist/extension-js-devtools/edge/pages/welcome.js +2 -2
  19. package/dist/extension-js-devtools/edge/scripts/logger-client.js +1 -1
  20. package/dist/extension-js-devtools/extension-js/chrome/events.ndjson +2 -0
  21. package/dist/extension-js-devtools/extension-js/chrome/ready.json +16 -0
  22. package/dist/extension-js-devtools/extension-js/chromium/events.ndjson +2 -0
  23. package/dist/extension-js-devtools/extension-js/chromium/ready.json +16 -0
  24. package/dist/extension-js-devtools/extension-js/edge/events.ndjson +2 -0
  25. package/dist/extension-js-devtools/extension-js/edge/ready.json +16 -0
  26. package/dist/extension-js-devtools/extension-js/firefox/events.ndjson +2 -0
  27. package/dist/extension-js-devtools/extension-js/firefox/ready.json +16 -0
  28. package/dist/extension-js-devtools/firefox/content_scripts/content-0.js +2 -2
  29. package/dist/extension-js-devtools/firefox/pages/centralized-logger.js +1 -1
  30. package/dist/extension-js-devtools/firefox/pages/welcome.js +2 -2
  31. package/dist/extension-js-devtools/firefox/scripts/logger-client.js +1 -1
  32. package/dist/extension-js-theme/extension-js/chrome/events.ndjson +2 -0
  33. package/dist/extension-js-theme/extension-js/chrome/ready.json +16 -0
  34. package/dist/extension-js-theme/extension-js/chromium/events.ndjson +2 -0
  35. package/dist/extension-js-theme/extension-js/chromium/ready.json +16 -0
  36. package/dist/extension-js-theme/extension-js/edge/events.ndjson +2 -0
  37. package/dist/extension-js-theme/extension-js/edge/ready.json +16 -0
  38. package/dist/extension-js-theme/extension-js/firefox/events.ndjson +4 -0
  39. package/dist/extension-js-theme/extension-js/firefox/ready.json +16 -0
  40. package/dist/feature-scripts-content-script-wrapper.js +19 -2
  41. package/dist/feature-scripts-content-script-wrapper.mjs +19 -2
  42. package/package.json +14 -5
  43. package/runtime/process-shim.cjs +49 -0
@@ -4,7 +4,9 @@ import { Writable } from "stream";
4
4
  import { rspack } from "@rspack/core";
5
5
  import { RspackDevServer } from "@rspack/dev-server";
6
6
  import { merge } from "webpack-merge";
7
+ import { WebSocket, WebSocketServer } from "ws";
7
8
  import { createPlaywrightMetadataWriter, autoExitModeEnabled, autoExitForceKill, loadCommandConfig, extensionJsRunnerError, portInUse, devServerStartTimeout, autoExitTriggered, loadCustomConfig, loadBrowserConfig, sanitize, resolveCompanionExtensionsConfig, getSpecialFoldersDataForProjectRoot } from "./946.mjs";
9
+ import { CONTROL_WS_PATH, clearControlToken, writeControlToken } from "./45.mjs";
8
10
  import { setupNoBrowserBannerOnFirstDone, webpackConfig, setupCompilerLifecycleHooks } from "./0~rspack-config.mjs";
9
11
  import * as __rspack_external_path from "path";
10
12
  import * as __rspack_external_crypto from "crypto";
@@ -47,65 +49,29 @@ function resolveInstanceIdOverride() {
47
49
  class PortManager {
48
50
  basePort;
49
51
  currentInstance = null;
50
- constructor(_browser, _projectPath, basePort = 8080){
52
+ constructor(basePort = 8080){
51
53
  this.basePort = basePort;
52
54
  }
53
- async allocatePorts(_browser, _projectPath, requestedPort) {
54
- try {
55
- const isValidRequested = 'number' == typeof requestedPort && requestedPort >= 0 && requestedPort < 65536;
56
- const base = isValidRequested ? requestedPort : this.basePort;
57
- const port = await findAvailablePortNear(base);
58
- const webSocketPort = await findAvailablePortNear(port + 1);
59
- const instanceId = resolveInstanceIdOverride() || __rspack_external_crypto.randomBytes(8).toString('hex');
60
- this.currentInstance = {
61
- instanceId,
62
- port,
63
- webSocketPort
64
- };
65
- return {
66
- port,
67
- webSocketPort,
68
- instanceId
69
- };
70
- } catch (error) {
71
- throw error;
72
- }
55
+ async allocatePorts(requestedPort) {
56
+ const isValidRequested = 'number' == typeof requestedPort && requestedPort >= 0 && requestedPort < 65536;
57
+ const base = isValidRequested ? requestedPort : this.basePort;
58
+ const port = await findAvailablePortNear(base);
59
+ const instanceId = resolveInstanceIdOverride() || __rspack_external_crypto.randomBytes(8).toString('hex');
60
+ this.currentInstance = {
61
+ instanceId,
62
+ port
63
+ };
64
+ return {
65
+ port,
66
+ instanceId
67
+ };
73
68
  }
74
69
  getCurrentInstance() {
75
70
  return this.currentInstance;
76
71
  }
77
- async updateExtensionId(extensionId) {
78
- if (this.currentInstance) this.currentInstance.extensionId = extensionId;
79
- }
80
72
  async terminateCurrentInstance() {
81
73
  this.currentInstance = null;
82
74
  }
83
- getPortInfo(allocation) {
84
- return `Port: ${allocation.port}, WebSocket: ${allocation.webSocketPort}, Instance: ${allocation.instanceId.slice(0, 8)}`;
85
- }
86
- async isPortInUse(port) {
87
- return new Promise((resolve)=>{
88
- const server = __rspack_external_net.createServer();
89
- server.once('error', ()=>resolve(true));
90
- server.once('listening', ()=>{
91
- server.close(()=>resolve(false));
92
- });
93
- server.listen(port, '127.0.0.1');
94
- });
95
- }
96
- async getRunningInstances() {
97
- return this.currentInstance ? [
98
- this.currentInstance
99
- ] : [];
100
- }
101
- async getStats() {
102
- return {
103
- total: this.currentInstance ? 1 : 0,
104
- running: this.currentInstance ? 1 : 0,
105
- terminated: 0,
106
- error: 0
107
- };
108
- }
109
75
  }
110
76
  function hasDependency(projectPath, dependency) {
111
77
  const findNearestPackageJsonDirectory = (startPath)=>{
@@ -207,9 +173,8 @@ function setupCleanupHandlers(devServer, portManager) {
207
173
  console.error('[Extension.js Runner] Uncaught exception.', error);
208
174
  await cleanup();
209
175
  });
210
- process.on('unhandledRejection', async (reason, promise)=>{
176
+ process.on('unhandledRejection', (reason, promise)=>{
211
177
  console.error('[Extension.js Runner] Unhandled rejection.', promise, reason);
212
- await cleanup();
213
178
  });
214
179
  const cancelAutoExit = setupAutoExit(process.env.EXTENSION_AUTO_EXIT_MS, process.env.EXTENSION_FORCE_KILL_MS, cleanup);
215
180
  const cancelAndCleanup = async ()=>{
@@ -218,12 +183,590 @@ function setupCleanupHandlers(devServer, portManager) {
218
183
  } catch {}
219
184
  await cleanup();
220
185
  };
221
- process.on('ERROR', cancelAndCleanup);
222
186
  process.on('SIGINT', cancelAndCleanup);
223
187
  process.on('SIGTERM', cancelAndCleanup);
224
188
  process.on('SIGHUP', cancelAndCleanup);
225
189
  return cancelAutoExit;
226
190
  }
191
+ const DEFAULT_RING_CAPACITY = 5000;
192
+ class LogRingBuffer {
193
+ capacity;
194
+ buf = [];
195
+ nextSeq = 1;
196
+ droppedSinceDrain = 0;
197
+ constructor(capacity = DEFAULT_RING_CAPACITY){
198
+ this.capacity = Math.max(1, Math.floor(capacity));
199
+ }
200
+ push(incoming) {
201
+ const event = {
202
+ ...incoming,
203
+ v: 1,
204
+ seq: this.nextSeq++
205
+ };
206
+ this.buf.push(event);
207
+ if (this.buf.length > this.capacity) {
208
+ this.buf.shift();
209
+ this.droppedSinceDrain++;
210
+ }
211
+ return event;
212
+ }
213
+ get size() {
214
+ return this.buf.length;
215
+ }
216
+ get bufferedFrom() {
217
+ return this.buf.length ? this.buf[0].seq : this.nextSeq;
218
+ }
219
+ get nextSequence() {
220
+ return this.nextSeq;
221
+ }
222
+ snapshot() {
223
+ return this.buf.slice();
224
+ }
225
+ since(seq) {
226
+ if (!Number.isFinite(seq)) return this.snapshot();
227
+ return this.buf.filter((e)=>e.seq > seq);
228
+ }
229
+ drainDropped() {
230
+ if (0 === this.droppedSinceDrain) return null;
231
+ const dropped = this.droppedSinceDrain;
232
+ this.droppedSinceDrain = 0;
233
+ return {
234
+ dropped,
235
+ reason: 'ring_overflow',
236
+ sinceSeq: this.bufferedFrom
237
+ };
238
+ }
239
+ }
240
+ const DEFAULTS = {
241
+ maxBytes: 8388608,
242
+ maxLines: 50000,
243
+ generations: 3,
244
+ flushIntervalMs: 250,
245
+ maxQueue: 20000
246
+ };
247
+ class LogsFileWriter {
248
+ opts;
249
+ queue = [];
250
+ bytes = 0;
251
+ lines = 0;
252
+ droppedToDisk = 0;
253
+ timer = null;
254
+ started = false;
255
+ constructor(options){
256
+ this.opts = {
257
+ ...DEFAULTS,
258
+ ...options
259
+ };
260
+ }
261
+ start() {
262
+ if (this.started) return;
263
+ this.started = true;
264
+ try {
265
+ __rspack_external_fs.mkdirSync(__rspack_external_path.dirname(this.opts.filePath), {
266
+ recursive: true
267
+ });
268
+ if (__rspack_external_fs.existsSync(this.opts.filePath)) this.rotate();
269
+ } catch {}
270
+ this.writeHeader(null);
271
+ this.timer = setInterval(()=>this.flush(), this.opts.flushIntervalMs);
272
+ if (this.timer.unref) this.timer.unref();
273
+ }
274
+ write(event) {
275
+ this.enqueue(JSON.stringify(event));
276
+ }
277
+ enqueue(line) {
278
+ this.queue.push(line);
279
+ if (this.queue.length > this.opts.maxQueue) {
280
+ this.queue.shift();
281
+ this.droppedToDisk++;
282
+ }
283
+ }
284
+ flush() {
285
+ if (!this.started || 0 === this.queue.length) return void this.maybeNoteDrops();
286
+ const batch = this.queue;
287
+ this.queue = [];
288
+ const payload = batch.join('\n') + '\n';
289
+ try {
290
+ __rspack_external_fs.appendFileSync(this.opts.filePath, payload, 'utf-8');
291
+ this.bytes += Buffer.byteLength(payload);
292
+ this.lines += batch.length;
293
+ } catch {
294
+ this.droppedToDisk += batch.length;
295
+ }
296
+ this.maybeNoteDrops();
297
+ if (this.bytes >= this.opts.maxBytes || this.lines >= this.opts.maxLines) {
298
+ this.rotate();
299
+ this.writeHeader(this.opts.runId);
300
+ }
301
+ }
302
+ close() {
303
+ if (this.timer) {
304
+ clearInterval(this.timer);
305
+ this.timer = null;
306
+ }
307
+ this.flush();
308
+ this.started = false;
309
+ }
310
+ maybeNoteDrops() {
311
+ if (0 === this.droppedToDisk) return;
312
+ const dropped = this.droppedToDisk;
313
+ this.droppedToDisk = 0;
314
+ try {
315
+ const sentinel = JSON.stringify({
316
+ v: 1,
317
+ type: 'gap',
318
+ reason: 'disk_slow',
319
+ dropped
320
+ }) + '\n';
321
+ __rspack_external_fs.appendFileSync(this.opts.filePath, sentinel, 'utf-8');
322
+ this.bytes += Buffer.byteLength(sentinel);
323
+ this.lines += 1;
324
+ } catch {}
325
+ }
326
+ writeHeader(rotatedFrom) {
327
+ const header = JSON.stringify({
328
+ v: 1,
329
+ type: 'header',
330
+ runId: this.opts.runId,
331
+ startedAt: new Date().toISOString(),
332
+ rotatedFrom
333
+ }) + '\n';
334
+ try {
335
+ __rspack_external_fs.writeFileSync(this.opts.filePath, header, 'utf-8');
336
+ this.bytes = Buffer.byteLength(header);
337
+ this.lines = 1;
338
+ } catch {
339
+ this.bytes = 0;
340
+ this.lines = 0;
341
+ }
342
+ }
343
+ rotatedName(n) {
344
+ return this.opts.filePath.replace(/\.ndjson$/, `.${n}.ndjson`);
345
+ }
346
+ rotate() {
347
+ const { generations } = this.opts;
348
+ try {
349
+ const oldest = this.rotatedName(generations);
350
+ if (__rspack_external_fs.existsSync(oldest)) __rspack_external_fs.rmSync(oldest, {
351
+ force: true
352
+ });
353
+ for(let n = generations - 1; n >= 1; n--){
354
+ const from = this.rotatedName(n);
355
+ if (__rspack_external_fs.existsSync(from)) __rspack_external_fs.renameSync(from, this.rotatedName(n + 1));
356
+ }
357
+ if (__rspack_external_fs.existsSync(this.opts.filePath)) __rspack_external_fs.renameSync(this.opts.filePath, this.rotatedName(1));
358
+ } catch {}
359
+ }
360
+ }
361
+ const actions_file_DEFAULTS = {
362
+ maxBytes: 8388608,
363
+ maxLines: 50000,
364
+ generations: 3
365
+ };
366
+ class ActionsFileWriter {
367
+ opts;
368
+ bytes = 0;
369
+ lines = 0;
370
+ started = false;
371
+ constructor(options){
372
+ this.opts = {
373
+ ...actions_file_DEFAULTS,
374
+ ...options
375
+ };
376
+ }
377
+ start() {
378
+ if (this.started) return;
379
+ this.started = true;
380
+ try {
381
+ __rspack_external_fs.mkdirSync(__rspack_external_path.dirname(this.opts.filePath), {
382
+ recursive: true
383
+ });
384
+ if (__rspack_external_fs.existsSync(this.opts.filePath)) this.rotate();
385
+ } catch {}
386
+ this.bytes = 0;
387
+ this.lines = 0;
388
+ }
389
+ write(record) {
390
+ if (!this.started) this.start();
391
+ const line = JSON.stringify(record) + '\n';
392
+ try {
393
+ __rspack_external_fs.appendFileSync(this.opts.filePath, line, 'utf-8');
394
+ this.bytes += Buffer.byteLength(line);
395
+ this.lines += 1;
396
+ } catch {
397
+ return;
398
+ }
399
+ if (this.bytes >= this.opts.maxBytes || this.lines >= this.opts.maxLines) {
400
+ this.rotate();
401
+ this.bytes = 0;
402
+ this.lines = 0;
403
+ }
404
+ }
405
+ close() {
406
+ this.started = false;
407
+ }
408
+ rotatedName(n) {
409
+ return this.opts.filePath.replace(/\.ndjson$/, `.${n}.ndjson`);
410
+ }
411
+ rotate() {
412
+ const { generations } = this.opts;
413
+ try {
414
+ const oldest = this.rotatedName(generations);
415
+ if (__rspack_external_fs.existsSync(oldest)) __rspack_external_fs.rmSync(oldest, {
416
+ force: true
417
+ });
418
+ for(let n = generations - 1; n >= 1; n--){
419
+ const from = this.rotatedName(n);
420
+ if (__rspack_external_fs.existsSync(from)) __rspack_external_fs.renameSync(from, this.rotatedName(n + 1));
421
+ }
422
+ if (__rspack_external_fs.existsSync(this.opts.filePath)) __rspack_external_fs.renameSync(this.opts.filePath, this.rotatedName(1));
423
+ } catch {}
424
+ }
425
+ }
426
+ function fnv1aHex(input) {
427
+ let hash = 0x811c9dc5;
428
+ for(let i = 0; i < input.length; i++){
429
+ hash ^= input.charCodeAt(i);
430
+ hash = Math.imul(hash, 0x01000193);
431
+ }
432
+ return (hash >>> 0).toString(16).padStart(8, '0');
433
+ }
434
+ const CLOSE_BAD_INSTANCE = 4001;
435
+ const CLOSE_BAD_HELLO = 4002;
436
+ const CLOSE_CONTROL_UNAVAILABLE = 4003;
437
+ const DEFAULT_CMD_TIMEOUT_MS = 5000;
438
+ const MAX_CMD_TIMEOUT_MS = 30000;
439
+ const CONTROL_OPS = new Set([
440
+ 'eval',
441
+ 'storage.get',
442
+ 'storage.set',
443
+ 'reload',
444
+ 'open',
445
+ 'tabs.query',
446
+ 'inspect'
447
+ ]);
448
+ class BridgeBroker {
449
+ instanceId;
450
+ runId;
451
+ engine;
452
+ ring;
453
+ file;
454
+ roles = new Map();
455
+ allowControl;
456
+ allowEval;
457
+ controlToken;
458
+ actions;
459
+ authorMode;
460
+ now;
461
+ setTimer;
462
+ clearTimer;
463
+ evalAllowed = new Map();
464
+ pending = new Map();
465
+ constructor(options){
466
+ this.instanceId = options.instanceId;
467
+ this.runId = options.runId;
468
+ this.engine = options.engine;
469
+ this.ring = options.ring ?? new LogRingBuffer();
470
+ this.file = options.file;
471
+ this.allowControl = options.allowControl ?? false;
472
+ this.allowEval = options.allowEval ?? false;
473
+ this.controlToken = options.controlToken;
474
+ this.actions = options.actions;
475
+ this.authorMode = options.authorMode ?? false;
476
+ this.now = options.now ?? (()=>Date.now());
477
+ this.setTimer = options.setTimer ?? ((fn, ms)=>setTimeout(fn, ms));
478
+ this.clearTimer = options.clearTimer ?? ((h)=>clearTimeout(h));
479
+ }
480
+ get consumerCount() {
481
+ return this.countRole('consumer');
482
+ }
483
+ get producerCount() {
484
+ return this.countRole('producer');
485
+ }
486
+ get controllerCount() {
487
+ return this.countRole('controller');
488
+ }
489
+ get pendingCount() {
490
+ return this.pending.size;
491
+ }
492
+ onFrame(conn, frame) {
493
+ switch(frame.type){
494
+ case 'hello':
495
+ this.onHello(conn, frame);
496
+ return;
497
+ case 'log':
498
+ if ('producer' === this.roles.get(conn)) this.ingestLog(frame.event);
499
+ return;
500
+ case 'command':
501
+ if ('controller' === this.roles.get(conn)) this.onCommand(conn, frame);
502
+ return;
503
+ case 'result':
504
+ if ('producer' === this.roles.get(conn)) this.onResult(frame);
505
+ return;
506
+ default:
507
+ return;
508
+ }
509
+ }
510
+ onClose(conn) {
511
+ this.roles.delete(conn);
512
+ this.evalAllowed.delete(conn);
513
+ for (const [cmdId, p] of this.pending)if (p.controller === conn) {
514
+ this.clearTimer(p.timer);
515
+ this.pending.delete(cmdId);
516
+ }
517
+ }
518
+ ingestLog(incoming) {
519
+ const event = this.ring.push(incoming);
520
+ this.file?.write(event);
521
+ this.fanOut({
522
+ type: 'log',
523
+ event
524
+ });
525
+ const gap = this.ring.drainDropped();
526
+ if (gap) this.fanOut({
527
+ type: 'gap',
528
+ dropped: gap.dropped,
529
+ reason: gap.reason,
530
+ sinceSeq: gap.sinceSeq
531
+ });
532
+ }
533
+ onHello(conn, hello) {
534
+ if (1 !== hello.v) return void conn.close(CLOSE_BAD_HELLO, 'unsupported envelope version');
535
+ if (hello.instanceId !== this.instanceId) return void conn.close(CLOSE_BAD_INSTANCE, 'instanceId mismatch');
536
+ if ('controller' === hello.role) {
537
+ if (!this.allowControl) return void conn.close(CLOSE_CONTROL_UNAVAILABLE, 'control channel not available');
538
+ this.roles.set(conn, 'controller');
539
+ this.evalAllowed.set(conn, this.allowEval && !!hello.token && !!this.controlToken && hello.token === this.controlToken);
540
+ conn.send(this.controllerReady());
541
+ return;
542
+ }
543
+ if ('producer' !== hello.role && 'consumer' !== hello.role) return void conn.close(CLOSE_BAD_HELLO, 'unknown role');
544
+ this.roles.set(conn, hello.role);
545
+ if ('consumer' === hello.role) {
546
+ const ready = {
547
+ type: 'ready',
548
+ runId: this.runId,
549
+ bufferedFrom: this.ring.bufferedFrom,
550
+ engine: this.engine
551
+ };
552
+ conn.send(ready);
553
+ for (const event of this.ring.snapshot())conn.send({
554
+ type: 'log',
555
+ event
556
+ });
557
+ }
558
+ }
559
+ controllerReady() {
560
+ const isFirefox = 'firefox' === this.engine;
561
+ return {
562
+ type: 'ready',
563
+ runId: this.runId,
564
+ bufferedFrom: this.ring.bufferedFrom,
565
+ engine: this.engine,
566
+ capabilities: {
567
+ eval: this.allowEval,
568
+ storage: this.allowControl,
569
+ reload: this.allowControl,
570
+ open: this.allowControl ? isFirefox ? [
571
+ 'popup',
572
+ 'options'
573
+ ] : [
574
+ 'popup',
575
+ 'options',
576
+ 'sidebar'
577
+ ] : [],
578
+ deepDom: 'chromium' === this.engine
579
+ }
580
+ };
581
+ }
582
+ onCommand(controller, cmd) {
583
+ const principal = 'controller';
584
+ const isEval = 'eval' === cmd.op;
585
+ const exprHash = isEval && 'string' == typeof cmd.args?.expression ? fnv1aHex(cmd.args.expression) : void 0;
586
+ const expr = isEval && this.authorMode && 'string' == typeof cmd.args?.expression ? cmd.args.expression : void 0;
587
+ const deny = (name, message)=>{
588
+ const result = {
589
+ type: 'result',
590
+ cmdId: cmd.cmdId,
591
+ ok: false,
592
+ error: {
593
+ name,
594
+ message
595
+ }
596
+ };
597
+ controller.send(result);
598
+ this.audit({
599
+ cmdId: cmd.cmdId,
600
+ op: cmd.op,
601
+ target: cmd.target,
602
+ ok: false,
603
+ durationMs: 0,
604
+ principal,
605
+ exprHash,
606
+ expr,
607
+ errorName: name
608
+ });
609
+ };
610
+ if (!CONTROL_OPS.has(cmd.op)) return void deny('BadRequest', `unknown op: ${String(cmd.op)}`);
611
+ if (isEval && !this.evalAllowed.get(controller)) return void deny('Forbidden', 'eval requires --allow-eval and a valid session token in the hello');
612
+ const executor = this.firstExecutor();
613
+ if (!executor) return void deny('Unavailable', 'no executor connected (is the dev session running?)');
614
+ const timeoutMs = Math.min(Math.max(cmd.timeoutMs ?? DEFAULT_CMD_TIMEOUT_MS, 1), MAX_CMD_TIMEOUT_MS);
615
+ const timer = this.setTimer(()=>this.onTimeout(cmd.cmdId), timeoutMs);
616
+ this.pending.set(cmd.cmdId, {
617
+ controller,
618
+ op: cmd.op,
619
+ target: cmd.target,
620
+ issuedAt: this.now(),
621
+ timer,
622
+ principal,
623
+ exprHash,
624
+ expr
625
+ });
626
+ executor.send({
627
+ type: 'command',
628
+ cmdId: cmd.cmdId,
629
+ op: cmd.op,
630
+ target: cmd.target,
631
+ args: cmd.args,
632
+ timeoutMs
633
+ });
634
+ }
635
+ onResult(result) {
636
+ const p = this.pending.get(result.cmdId);
637
+ if (!p) return;
638
+ this.clearTimer(p.timer);
639
+ this.pending.delete(result.cmdId);
640
+ p.controller.send(result);
641
+ this.audit({
642
+ cmdId: result.cmdId,
643
+ op: p.op,
644
+ target: p.target,
645
+ ok: result.ok,
646
+ durationMs: result.durationMs ?? this.now() - p.issuedAt,
647
+ principal: p.principal,
648
+ exprHash: p.exprHash,
649
+ expr: p.expr,
650
+ errorName: result.ok ? void 0 : result.error?.name
651
+ });
652
+ }
653
+ onTimeout(cmdId) {
654
+ const p = this.pending.get(cmdId);
655
+ if (!p) return;
656
+ this.pending.delete(cmdId);
657
+ const result = {
658
+ type: 'result',
659
+ cmdId,
660
+ ok: false,
661
+ error: {
662
+ name: 'Timeout',
663
+ message: 'command timed out'
664
+ }
665
+ };
666
+ p.controller.send(result);
667
+ this.audit({
668
+ cmdId,
669
+ op: p.op,
670
+ target: p.target,
671
+ ok: false,
672
+ durationMs: this.now() - p.issuedAt,
673
+ principal: p.principal,
674
+ exprHash: p.exprHash,
675
+ expr: p.expr,
676
+ errorName: 'Timeout'
677
+ });
678
+ }
679
+ firstExecutor() {
680
+ for (const [conn, role] of this.roles)if ('producer' === role) return conn;
681
+ return null;
682
+ }
683
+ audit(record) {
684
+ if (!this.actions) return;
685
+ const line = {
686
+ v: 1,
687
+ ts: new Date(this.now()).toISOString(),
688
+ ...record
689
+ };
690
+ if (void 0 === line.exprHash) delete line.exprHash;
691
+ if (void 0 === line.expr) delete line.expr;
692
+ if (void 0 === line.errorName) delete line.errorName;
693
+ if (void 0 === line.durationMs) delete line.durationMs;
694
+ this.actions.write(line);
695
+ }
696
+ fanOut(frame) {
697
+ for (const [conn, role] of this.roles)if ('consumer' === role) try {
698
+ conn.send(frame);
699
+ } catch {}
700
+ }
701
+ countRole(role) {
702
+ let n = 0;
703
+ for (const r of this.roles.values())if (r === role) n++;
704
+ return n;
705
+ }
706
+ }
707
+ const SLOW_CONSUMER_BYTES = 8388608;
708
+ const CLOSE_SLOW_CONSUMER = 4008;
709
+ let connSeq = 0;
710
+ function startControlServer(options) {
711
+ const { broker } = options;
712
+ const host = options.host ?? '127.0.0.1';
713
+ const path = options.path ?? CONTROL_WS_PATH;
714
+ return new Promise((resolve, reject)=>{
715
+ const wss = new WebSocketServer({
716
+ host,
717
+ port: options.port ?? 0,
718
+ path
719
+ });
720
+ wss.on('error', reject);
721
+ wss.on('connection', (socket)=>{
722
+ const conn = {
723
+ id: `c${++connSeq}`,
724
+ send (frame) {
725
+ if (socket.readyState !== WebSocket.OPEN) return;
726
+ if (socket.bufferedAmount > SLOW_CONSUMER_BYTES) {
727
+ try {
728
+ socket.close(CLOSE_SLOW_CONSUMER, 'slow consumer');
729
+ } catch {}
730
+ return;
731
+ }
732
+ try {
733
+ socket.send(JSON.stringify(frame));
734
+ } catch {}
735
+ },
736
+ close (code, reason) {
737
+ try {
738
+ socket.close(code, reason);
739
+ } catch {}
740
+ }
741
+ };
742
+ socket.on('message', (data)=>{
743
+ let frame;
744
+ try {
745
+ frame = JSON.parse(data.toString());
746
+ } catch {
747
+ return;
748
+ }
749
+ broker.onFrame(conn, frame);
750
+ });
751
+ socket.on('close', ()=>broker.onClose(conn));
752
+ socket.on('error', ()=>broker.onClose(conn));
753
+ });
754
+ wss.on('listening', ()=>{
755
+ const address = wss.address();
756
+ const port = 'object' == typeof address && address ? address.port : 0;
757
+ resolve({
758
+ port,
759
+ broker,
760
+ close: ()=>new Promise((res)=>{
761
+ for (const client of wss.clients)try {
762
+ client.terminate();
763
+ } catch {}
764
+ wss.close(()=>res());
765
+ })
766
+ });
767
+ });
768
+ });
769
+ }
227
770
  function shouldWriteAssetToDisk(filePath) {
228
771
  return !/(?:^|[/\\])manifest\.json$/i.test(filePath);
229
772
  }
@@ -363,13 +906,12 @@ function installManifestDiskWriteGuard(manifestOutputPath) {
363
906
  async function dev_server_devServer(projectStructure, devOptions) {
364
907
  process.env.EXTENSION_BROWSER_LAUNCH_ENABLED = devOptions.noBrowser ? '0' : '1';
365
908
  const { manifestPath, packageJsonPath } = projectStructure;
366
- __rspack_external_path.dirname(manifestPath);
367
909
  const packageJsonDir = __rspack_external_path.dirname(packageJsonPath);
368
910
  const commandConfig = await loadCommandConfig(packageJsonDir, 'dev');
369
911
  const browserConfig = await loadBrowserConfig(packageJsonDir, devOptions.browser);
370
- const portManager = new PortManager(devOptions.browser, packageJsonDir, 8080);
912
+ const portManager = new PortManager(8080);
371
913
  const desiredPort = 'string' == typeof devOptions.port ? parseInt(devOptions.port, 10) : devOptions.port;
372
- const portAllocation = await portManager.allocatePorts(devOptions.browser, packageJsonDir, desiredPort);
914
+ const portAllocation = await portManager.allocatePorts(desiredPort);
373
915
  const currentInstance = portManager.getCurrentInstance();
374
916
  if (!currentInstance) throw new Error('Failed to create instance');
375
917
  const port = portAllocation.port;
@@ -383,6 +925,56 @@ async function dev_server_devServer(projectStructure, devOptions) {
383
925
  process.env.EXTENSION_DEV_SERVER_HOST = devServerHost;
384
926
  process.env.EXTENSION_DEV_SERVER_PORT = String(port);
385
927
  process.env.EXTENSION_DEV_SERVER_PATH = '/ws';
928
+ const browserName = String(devOptions.browser || 'chromium');
929
+ const bridgeLogsRelPath = __rspack_external_path.join('dist', 'extension-js', browserName, 'logs.ndjson');
930
+ const bridgeLogFile = new LogsFileWriter({
931
+ filePath: __rspack_external_path.join(packageJsonDir, bridgeLogsRelPath),
932
+ runId: currentInstance.instanceId
933
+ });
934
+ const allowControl = Boolean(devOptions.allowControl);
935
+ const allowEval = Boolean(devOptions.allowEval);
936
+ const authorMode = Boolean(devOptions.authorMode) || 'development' === process.env.EXTENSION_AUTHOR_MODE;
937
+ const bridgeActionsFile = allowControl ? new ActionsFileWriter({
938
+ filePath: __rspack_external_path.join(packageJsonDir, 'dist', 'extension-js', browserName, 'actions.ndjson')
939
+ }) : void 0;
940
+ const bridgeControlToken = allowControl && allowEval ? writeControlToken(packageJsonDir) : void 0;
941
+ const bridgeBroker = new BridgeBroker({
942
+ instanceId: currentInstance.instanceId,
943
+ runId: currentInstance.instanceId,
944
+ engine: browserName.includes('firefox') || browserName.includes('gecko') ? 'firefox' : 'chromium',
945
+ ring: new LogRingBuffer(),
946
+ file: bridgeLogFile,
947
+ allowControl,
948
+ allowEval,
949
+ controlToken: bridgeControlToken,
950
+ actions: bridgeActionsFile,
951
+ authorMode
952
+ });
953
+ let bridgeControlPort = null;
954
+ try {
955
+ bridgeLogFile.start();
956
+ bridgeActionsFile?.start();
957
+ const controlServer = await startControlServer({
958
+ broker: bridgeBroker,
959
+ host: devServerHost
960
+ });
961
+ bridgeControlPort = controlServer.port;
962
+ } catch {
963
+ try {
964
+ bridgeLogFile.close();
965
+ } catch {}
966
+ }
967
+ process.env.EXTENSION_INSTANCE_ID = currentInstance.instanceId;
968
+ process.env.EXTENSION_CONTROL_PORT = null != bridgeControlPort ? String(bridgeControlPort) : '';
969
+ process.once('exit', ()=>{
970
+ try {
971
+ bridgeLogFile.close();
972
+ } catch {}
973
+ try {
974
+ bridgeActionsFile?.close();
975
+ } catch {}
976
+ if (bridgeControlToken) clearControlToken(packageJsonDir);
977
+ });
386
978
  const safeBrowserConfig = sanitize(browserConfig);
387
979
  const safeCommandConfig = sanitize(commandConfig);
388
980
  const safeDevOptions = sanitize(devOptions);
@@ -401,6 +993,9 @@ async function dev_server_devServer(projectStructure, devOptions) {
401
993
  browser: devOptions.browser,
402
994
  mode: 'development',
403
995
  instanceId: currentInstance.instanceId,
996
+ controlPort: bridgeControlPort,
997
+ controlPath: CONTROL_WS_PATH,
998
+ logsPath: bridgeLogsRelPath,
404
999
  port: portAllocation.port,
405
1000
  output: {
406
1001
  clean: false,
@@ -420,7 +1015,11 @@ async function dev_server_devServer(projectStructure, devOptions) {
420
1015
  command: 'dev',
421
1016
  distPath: __rspack_external_path.join(packageJsonDir, 'dist', String(devOptions.browser || 'chromium')),
422
1017
  manifestPath,
423
- port
1018
+ port,
1019
+ instanceId: currentInstance.instanceId,
1020
+ controlPort: bridgeControlPort,
1021
+ controlPath: CONTROL_WS_PATH,
1022
+ logsPath: bridgeLogsRelPath
424
1023
  });
425
1024
  setupCompilerLifecycleHooks(compiler);
426
1025
  if (devOptions.noBrowser) setupNoBrowserBannerOnFirstDone({
@@ -456,8 +1055,10 @@ async function dev_server_devServer(projectStructure, devOptions) {
456
1055
  __rspack_external_path.join(packageJsonDir, 'dist', '**/*')
457
1056
  ],
458
1057
  ignoreInitial: true,
459
- usePolling: true,
460
- interval: 1000
1058
+ ...'true' === process.env.EXTENSION_WATCH_POLL ? {
1059
+ usePolling: true,
1060
+ interval: parseInt(String(process.env.EXTENSION_WATCH_POLL_INTERVAL || '1000'), 10)
1061
+ } : {}
461
1062
  }
462
1063
  },
463
1064
  client: false,