@browserless.io/browserless 2.15.0 → 2.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserless.io/browserless",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "license": "SSPL",
5
5
  "description": "The browserless platform",
6
6
  "author": "browserless.io",
@@ -51,7 +51,7 @@
51
51
  "debug": "^4.3.5",
52
52
  "del": "^7.0.0",
53
53
  "enjoi": "^9.0.1",
54
- "file-type": "^19.0.0",
54
+ "file-type": "^19.1.1",
55
55
  "get-port": "^7.1.0",
56
56
  "gradient-string": "^2.0.0",
57
57
  "http-proxy": "^1.18.1",
@@ -62,7 +62,7 @@
62
62
  "playwright-1.43": "npm:playwright-core@1.43.1",
63
63
  "playwright-1.44": "npm:playwright-core@1.44.1",
64
64
  "playwright-core": "^1.45.1",
65
- "puppeteer-core": "^22.12.1",
65
+ "puppeteer-core": "^22.13.0",
66
66
  "puppeteer-extra": "^3.3.6",
67
67
  "puppeteer-extra-plugin-stealth": "^2.11.2",
68
68
  "queue": "^7.0.0",
@@ -76,10 +76,10 @@
76
76
  "@types/http-proxy": "^1.17.14",
77
77
  "@types/micromatch": "^4.0.9",
78
78
  "@types/mocha": "^10.0.7",
79
- "@types/node": "^20.14.9",
79
+ "@types/node": "^20.14.10",
80
80
  "@types/sinon": "^17.0.3",
81
- "@typescript-eslint/eslint-plugin": "^7.15.0",
82
- "@typescript-eslint/parser": "^7.15.0",
81
+ "@typescript-eslint/eslint-plugin": "^7.16.0",
82
+ "@typescript-eslint/parser": "^7.16.0",
83
83
  "assert": "^2.0.0",
84
84
  "chai": "^5.1.1",
85
85
  "cross-env": "^7.0.3",
@@ -90,10 +90,10 @@
90
90
  "eslint-plugin-typescript-sort-keys": "^3.2.0",
91
91
  "extract-zip": "^2.0.1",
92
92
  "gunzip-maybe": "^1.4.2",
93
- "marked": "^13.0.1",
93
+ "marked": "^13.0.2",
94
94
  "mocha": "^10.6.0",
95
95
  "move-file": "^3.1.0",
96
- "prettier": "^3.3.2",
96
+ "prettier": "^3.3.3",
97
97
  "sinon": "^18.0.0",
98
98
  "ts-node": "^10.9.2",
99
99
  "typescript": "^5.5.3",
@@ -12,7 +12,7 @@ const moduleMain = path.normalize(import.meta.url).endsWith(process.argv[1]);
12
12
 
13
13
  /**
14
14
  * Find an exported interface in a TypeScript AST
15
- *
15
+ *
16
16
  * @param {ts.Node} node The node to search for the exported interface
17
17
  * @param {string} interfaceName The name of the interface to search for
18
18
  * @returns {ts.InterfaceDeclaration | ts.Identifier | null}
@@ -29,7 +29,11 @@ const findExportedInterface = (node, interfaceName) => {
29
29
  }
30
30
 
31
31
  // Check for re-exported interfaces
32
- if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
32
+ if (
33
+ ts.isExportDeclaration(node) &&
34
+ node.exportClause &&
35
+ ts.isNamedExports(node.exportClause)
36
+ ) {
33
37
  const elements = node.exportClause.elements;
34
38
  for (const element of elements) {
35
39
  if (element.name.text === interfaceName) {
@@ -50,7 +54,7 @@ const findExportedInterface = (node, interfaceName) => {
50
54
 
51
55
  /**
52
56
  * Creates an standard JSON schema file for each route (see https://json-schema.org/specification)
53
- *
57
+ *
54
58
  * @param {string[]} externalHTTPRoutes Additional HTTP routes to parse
55
59
  * @param {string[]} externalWebSocketRoutes Additional WS routes to parse
56
60
  */
@@ -8,12 +8,12 @@ import {
8
8
  chromeExecutablePath,
9
9
  noop,
10
10
  once,
11
+ ublockPath,
11
12
  } from '@browserless.io/browserless';
12
13
  import puppeteer, { Browser, Page, Target } from 'puppeteer-core';
13
14
  import { Duplex } from 'stream';
14
15
  import { EventEmitter } from 'events';
15
16
  import StealthPlugin from 'puppeteer-extra-plugin-stealth';
16
- import { fileURLToPath } from 'url';
17
17
  import getPort from 'get-port';
18
18
  import httpProxy from 'http-proxy';
19
19
  import path from 'path';
@@ -170,11 +170,29 @@ export class ChromiumCDP extends EventEmitter {
170
170
  return this.browser?.process() || null;
171
171
  }
172
172
 
173
- public async launch(laucherOpts: BrowserLauncherOptions): Promise<Browser> {
174
- const { options, stealth } = laucherOpts;
173
+ public async launch({
174
+ options,
175
+ stealth,
176
+ }: BrowserLauncherOptions): Promise<Browser> {
175
177
  this.port = await getPort();
176
178
  this.logger.info(`${this.constructor.name} got open port ${this.port}`);
177
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
179
+
180
+ const extensionLaunchArgs = options.args?.find((a) =>
181
+ a.startsWith('--load-extension'),
182
+ );
183
+
184
+ // Remove extension flags as we recompile them below with our own
185
+ options.args = options.args?.filter(
186
+ (a) =>
187
+ !a.startsWith('--load-extension') &&
188
+ !a.startsWith('--disable-extensions-except'),
189
+ );
190
+
191
+ const extensions = [
192
+ this.blockAds ? ublockPath : null,
193
+ extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null,
194
+ ].filter((_) => !!_);
195
+
178
196
  const finalOptions = {
179
197
  ...options,
180
198
  args: [
@@ -186,21 +204,10 @@ export class ChromiumCDP extends EventEmitter {
186
204
  executablePath: this.executablePath,
187
205
  };
188
206
 
189
- if (this.blockAds) {
190
- // Necessary to load extensions
191
- finalOptions.headless = false;
192
-
193
- const loadExtensionPaths: string = path.join(
194
- __dirname,
195
- '..',
196
- '..',
197
- 'extensions',
198
- 'ublock',
199
- );
200
-
207
+ if (extensions.length) {
201
208
  finalOptions.args.push(
202
- '--load-extension=' + loadExtensionPaths,
203
- '--disable-extensions-except=' + loadExtensionPaths,
209
+ '--load-extension=' + extensions.join(','),
210
+ '--disable-extensions-except=' + extensions.join(','),
204
211
  );
205
212
  }
206
213
 
@@ -27,8 +27,10 @@ class BasePlaywright extends EventEmitter {
27
27
  protected proxy = httpProxy.createProxyServer();
28
28
  protected browser: playwright.BrowserServer | null = null;
29
29
  protected browserWSEndpoint: string | null = null;
30
- protected playwrightBrowserType: PlaywrightBrowserTypes = PlaywrightBrowserTypes.chromium;
31
- protected executablePath = () => playwright[this.playwrightBrowserType].executablePath();
30
+ protected playwrightBrowserType: PlaywrightBrowserTypes =
31
+ PlaywrightBrowserTypes.chromium;
32
+ protected executablePath = () =>
33
+ playwright[this.playwrightBrowserType].executablePath();
32
34
 
33
35
  constructor({
34
36
  config,
@@ -107,17 +109,22 @@ class BasePlaywright extends EventEmitter {
107
109
  `${this.constructor.name} hasn't been launched yet!`,
108
110
  );
109
111
  }
110
- const browser = await playwright[this.playwrightBrowserType].connect(this.browserWSEndpoint);
112
+ const browser = await playwright[this.playwrightBrowserType].connect(
113
+ this.browserWSEndpoint,
114
+ );
111
115
  return await browser.newPage();
112
116
  }
113
117
 
114
- public async launch(laucherOpts: BrowserLauncherOptions): Promise<playwright.BrowserServer> {
118
+ public async launch(
119
+ laucherOpts: BrowserLauncherOptions,
120
+ ): Promise<playwright.BrowserServer> {
115
121
  const { options, pwVersion } = laucherOpts;
116
122
  this.logger.info(`Launching ${this.constructor.name} Handler`);
117
123
 
118
124
  const opts = this.makeLaunchOptions(options);
119
125
  const versionedPw = await this.config.loadPwVersion(pwVersion!);
120
- const browser = await versionedPw[this.playwrightBrowserType].launchServer(opts);
126
+ const browser =
127
+ await versionedPw[this.playwrightBrowserType].launchServer(opts);
121
128
  const browserWSEndpoint = browser.wsEndpoint();
122
129
 
123
130
  this.logger.info(
@@ -535,7 +535,7 @@ export class BrowserManager {
535
535
  options: launchOptions as BrowserServerOptions,
536
536
  pwVersion,
537
537
  req,
538
- stealth: launchOptions?.stealth
538
+ stealth: launchOptions?.stealth,
539
539
  });
540
540
  await this.hooks.browser({ browser, req });
541
541
 
@@ -22,6 +22,7 @@ describe(`Limiter`, () => {
22
22
  webHooks.callRejectAlertURL.resetHistory();
23
23
  webHooks.callTimeoutAlertURL.resetHistory();
24
24
  webHooks.callErrorAlertURL.resetHistory();
25
+
25
26
  hooks.before.resetHistory();
26
27
  hooks.after.resetHistory();
27
28
  hooks.browser.resetHistory();
@@ -119,6 +120,32 @@ describe(`Limiter`, () => {
119
120
  expect(handlerTwo.calledOnce).to.be.true;
120
121
  });
121
122
 
123
+ it('continues to process jobs even if an earlier job errors', (d) => {
124
+ const config = new Config();
125
+ const monitoring = new Monitoring(config);
126
+ const metrics = new Metrics();
127
+
128
+ config.setConcurrent(1);
129
+ config.setQueued(1);
130
+ config.setTimeout(-1);
131
+
132
+ const limiter = new Limiter(config, metrics, monitoring, webHooks, hooks);
133
+ const errorJob = () =>
134
+ Promise.reject(new Error('Danger, danger. High voltage!'));
135
+ const okJob = spy();
136
+
137
+ const jobOne = limiter.limit(errorJob, asyncNoop, asyncNoop, noop);
138
+ const jobTwo = limiter.limit(okJob, asyncNoop, asyncNoop, noop);
139
+
140
+ jobOne();
141
+ jobTwo();
142
+
143
+ limiter.addEventListener('end', () => {
144
+ expect(okJob.calledOnce).to.be.true;
145
+ d(undefined);
146
+ });
147
+ });
148
+
122
149
  it('bubbles up errors', async () => {
123
150
  const config = new Config();
124
151
  const monitoring = new Monitoring(config);
package/src/logger.ts CHANGED
@@ -23,7 +23,7 @@ export class Logger {
23
23
  }
24
24
 
25
25
  protected get reqInfo() {
26
- return this.request ? this.request.socket.remoteAddress ?? 'Unknown' : '';
26
+ return this.request ? (this.request.socket.remoteAddress ?? 'Unknown') : '';
27
27
  }
28
28
 
29
29
  public trace(...messages: unknown[]) {
package/src/utils.ts CHANGED
@@ -23,6 +23,7 @@ import { Page } from 'puppeteer-core';
23
23
  import { ServerResponse } from 'http';
24
24
  import crypto from 'crypto';
25
25
  import debug from 'debug';
26
+ import { fileURLToPath } from 'url';
26
27
  import gradient from 'gradient-string';
27
28
  import { homedir } from 'os';
28
29
  import path from 'path';
@@ -33,6 +34,8 @@ const isHTTP = (
33
34
  return (writeable as ServerResponse).writeHead !== undefined;
34
35
  };
35
36
 
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+
36
39
  const getAuthHeaderToken = (header: string) => {
37
40
  if (header.startsWith('Basic')) {
38
41
  const username = header.split(/\s+/).pop() || '';
@@ -475,10 +478,11 @@ export const queryParamsToObject = (
475
478
  ): Record<string, unknown> =>
476
479
  [...params.entries()].reduce(
477
480
  (accum, [key, value]) => {
478
- accum[key] = value;
481
+ accum[key] =
482
+ value === '' || value === undefined || value === null ? true : value;
479
483
  return accum;
480
484
  },
481
- {} as Record<string, string>,
485
+ {} as ReturnType<typeof queryParamsToObject>,
482
486
  );
483
487
 
484
488
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -844,3 +848,5 @@ export const getCDPClient = (page: Page): CDPSession => {
844
848
 
845
849
  return typeof c === 'function' ? c.call(page) : c;
846
850
  };
851
+
852
+ export const ublockPath = path.join(__dirname, '..', 'extensions', 'ublock');