@onting/browser 0.0.0-0 → 0.1.0-main.202606180757.8b506d4
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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/browser.d.ts +6 -1
- package/dist/browser.js +753 -3
- package/dist/browser.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/package.json +27 -4
- package/src/bin-open/index.ts +254 -0
- package/src/bin-open/private/buildWebDriver.ts +100 -0
- package/src/bin-open/private/createSequencer.ts +46 -0
- package/src/bin-open/private/delta.ts +13 -0
- package/src/bin-open/private/findChromeDriverBin.ts +21 -0
- package/src/bin-open/private/findEdgeDriverBin.ts +21 -0
- package/src/bin-open/private/findGeckoDriverBin.ts +21 -0
- package/src/bin-open/private/findHostIP.ts +15 -0
- package/src/bin-open/private/findLocalIP.ts +15 -0
- package/src/bin-open/private/findSafariDriverBin.ts +17 -0
- package/src/bin-open/private/isWSL2.ts +20 -0
- package/src/bin-open/private/shortenRealmId.ts +7 -0
- package/dist/browser.mjs +0 -3
- package/dist/browser.mjs.map +0 -1
- /package/dist/{browser.d.mts → index.d.ts} +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onting/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0-main.202606180757.8b506d4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"open": "./src/bin-open/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24.12.0"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
8
15
|
"main": "./dist/browser.js",
|
|
9
16
|
"typings": "./dist/browser.d.ts",
|
|
10
17
|
"exports": {
|
|
11
|
-
".": {
|
|
18
|
+
"./browser.js": {
|
|
12
19
|
"types": "./dist/browser.d.ts",
|
|
13
20
|
"default": "./dist/browser.js"
|
|
21
|
+
},
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
14
25
|
}
|
|
15
26
|
},
|
|
16
27
|
"files": [
|
|
17
28
|
"./*.js",
|
|
18
|
-
"./dist/"
|
|
29
|
+
"./dist/",
|
|
30
|
+
"./src/bin-open/"
|
|
19
31
|
],
|
|
20
32
|
"scripts": {
|
|
21
33
|
"build": "tsup",
|
|
@@ -57,6 +69,7 @@
|
|
|
57
69
|
"@tsconfig/recommended": "^1.0.13",
|
|
58
70
|
"@tsconfig/strictest": "^2.0.8",
|
|
59
71
|
"@types/node": "^25.9.3",
|
|
72
|
+
"@types/selenium-webdriver": "^4.35.6",
|
|
60
73
|
"esbuild": "^0.28.1",
|
|
61
74
|
"escape-string-regexp": "^5.0.0",
|
|
62
75
|
"expect": "^30.4.1",
|
|
@@ -66,5 +79,15 @@
|
|
|
66
79
|
"typescript": "^6.0.3"
|
|
67
80
|
},
|
|
68
81
|
"license": "MIT",
|
|
69
|
-
"pinDependencies": {}
|
|
82
|
+
"pinDependencies": {},
|
|
83
|
+
"dependencies": {
|
|
84
|
+
"@onting/browser": "^0.1.0-main.202606180757.8b506d4",
|
|
85
|
+
"@onting/rpc": "^0.1.0-main.202606142148.67b5d79",
|
|
86
|
+
"@onting/selenium-webdriver-message-port": "^0.2.0",
|
|
87
|
+
"@onting/stub": "^0.1.0-main.202606180715.1310683",
|
|
88
|
+
"commander": "^15.0.0",
|
|
89
|
+
"find-up": "^8.0.0",
|
|
90
|
+
"message-port-rpc": "^3.0.1",
|
|
91
|
+
"selenium-webdriver": "^4.45.0"
|
|
92
|
+
}
|
|
70
93
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
|
|
5
|
+
import { listen } from '@onting/rpc/server.js';
|
|
6
|
+
import { viaBiDi } from '@onting/selenium-webdriver-message-port/host.js';
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { BrowsingContext, error as SeleniumWebDriverError } from 'selenium-webdriver';
|
|
10
|
+
import getScriptManagerInstance from 'selenium-webdriver/bidi/scriptManager.js';
|
|
11
|
+
import buildWebDriver from './private/buildWebDriver.ts';
|
|
12
|
+
import createSequencer from './private/createSequencer.ts';
|
|
13
|
+
import delta from './private/delta.ts';
|
|
14
|
+
import isWSL2 from './private/isWSL2.ts';
|
|
15
|
+
import shortenRealmId from './private/shortenRealmId.ts';
|
|
16
|
+
|
|
17
|
+
program.name('@onting/browser').description('Run browser with RPC stub');
|
|
18
|
+
|
|
19
|
+
program.arguments('[url]');
|
|
20
|
+
|
|
21
|
+
program.option('--chrome', 'run Chrome/Chromium');
|
|
22
|
+
program.option('--edge', 'run Edge');
|
|
23
|
+
program.option('--firefox', 'run Firefox');
|
|
24
|
+
program.option('--safari', 'run Safari');
|
|
25
|
+
program.option('--stub <stub-package-or-path>', 'load stub from the package or path', '@onting/stub');
|
|
26
|
+
program.option('--pipe', 'pipe WebDriver output to stdio');
|
|
27
|
+
program.option('--wsl', 'run browser on Windows (if under WSL2)');
|
|
28
|
+
|
|
29
|
+
program.parse(process.argv);
|
|
30
|
+
|
|
31
|
+
const opts =
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
33
|
+
program.opts() satisfies {} as {
|
|
34
|
+
chrome: boolean | undefined;
|
|
35
|
+
edge: boolean | undefined;
|
|
36
|
+
firefox: boolean | undefined;
|
|
37
|
+
pipe: boolean | undefined;
|
|
38
|
+
safari: boolean | undefined;
|
|
39
|
+
stub: string;
|
|
40
|
+
wsl: boolean | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let useWindowsBinary = !!opts.wsl;
|
|
44
|
+
|
|
45
|
+
if (opts.wsl && !(await isWSL2())) {
|
|
46
|
+
console.warn('Not running under WSL2, ignoring --wsl.');
|
|
47
|
+
|
|
48
|
+
useWindowsBinary = false;
|
|
49
|
+
} else if (os.platform() === 'win32') {
|
|
50
|
+
useWindowsBinary = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const webDriver = await buildWebDriver(
|
|
54
|
+
opts.edge ? 'edge' : opts.firefox ? 'firefox' : opts.safari ? 'safari' : 'chrome',
|
|
55
|
+
{ pipeStdio: !!opts.pipe, useWindowsBinary }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Patch WebSocket so to handle large amount of ScriptManager.
|
|
59
|
+
const { socket } = await webDriver.getBidi();
|
|
60
|
+
|
|
61
|
+
'setMaxListeners' in socket && typeof socket.setMaxListeners === 'function' && socket.setMaxListeners(100);
|
|
62
|
+
|
|
63
|
+
type RealmInfo = {
|
|
64
|
+
readonly browsingContext: string;
|
|
65
|
+
readonly origin: string;
|
|
66
|
+
readonly realmId: string;
|
|
67
|
+
readonly realmType: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ScriptManager = {
|
|
71
|
+
close(): Promise<void>;
|
|
72
|
+
getAllRealms(): Promise<readonly RealmInfo[]>;
|
|
73
|
+
onRealmCreated(callback: () => void): Promise<string>;
|
|
74
|
+
onRealmDestroyed(callback: () => void): Promise<string>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type ActiveRealmContextReadWrite = {
|
|
78
|
+
messagePortPromise: Promise<MessagePort>;
|
|
79
|
+
realmInfo: RealmInfo;
|
|
80
|
+
scriptManagerPromise: Promise<ScriptManager>;
|
|
81
|
+
|
|
82
|
+
abort(): void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type ActiveRealmContext = Readonly<ActiveRealmContextReadWrite>;
|
|
86
|
+
|
|
87
|
+
async function attachRealm(realmInfo: RealmInfo): Promise<void> {
|
|
88
|
+
const { realmId } = realmInfo;
|
|
89
|
+
|
|
90
|
+
if (activeRealms.has(realmId)) {
|
|
91
|
+
throw new Error(`Realm "${realmId}" has already attached`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(
|
|
95
|
+
`[${shortenRealmId(realmId)}] Attach "${realmInfo.realmType}" realm of browsing context "${realmInfo.browsingContext}" at ${realmInfo.origin}`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const abortController = new AbortController();
|
|
99
|
+
|
|
100
|
+
const scriptManagerPromise = getScriptManagerInstance(
|
|
101
|
+
realmInfo.browsingContext,
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
webDriver as any
|
|
104
|
+
) as unknown as Promise<ScriptManager>;
|
|
105
|
+
|
|
106
|
+
const messagePortPromise = scriptManagerPromise
|
|
107
|
+
.then(scriptManager => {
|
|
108
|
+
if (abortController.signal.aborted) {
|
|
109
|
+
throw new Error('Aborted');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return viaBiDi(scriptManager, { realmId });
|
|
113
|
+
})
|
|
114
|
+
.then(({ messagePort }) => messagePort);
|
|
115
|
+
|
|
116
|
+
const entry: ActiveRealmContextReadWrite = {
|
|
117
|
+
abort: abortController.abort.bind(abortController),
|
|
118
|
+
messagePortPromise,
|
|
119
|
+
realmInfo,
|
|
120
|
+
// TODO: It seems `selenium-webdriver@4.44.0` is bugged.
|
|
121
|
+
// If we use a shared `ScriptManager`, we will receive channel messages more than once.
|
|
122
|
+
// It seems if `onMessage()` is called twice, `selenium-webdriver` will call every `onMessage()` twice as well (4 times in total).
|
|
123
|
+
scriptManagerPromise
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
activeRealms.set(realmId, entry);
|
|
127
|
+
|
|
128
|
+
const teardown = listen(
|
|
129
|
+
// Security risk: intentionally load code from user-supplied path.
|
|
130
|
+
(await import(opts.stub)).default,
|
|
131
|
+
{
|
|
132
|
+
browsingContext: await BrowsingContext(webDriver, { browsingContextId: realmInfo.browsingContext }),
|
|
133
|
+
webDriver
|
|
134
|
+
},
|
|
135
|
+
await entry.messagePortPromise
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
abortController.signal.addEventListener('abort', () => {
|
|
139
|
+
(async () => {
|
|
140
|
+
try {
|
|
141
|
+
(await entry.messagePortPromise).close();
|
|
142
|
+
} catch {}
|
|
143
|
+
})();
|
|
144
|
+
|
|
145
|
+
(async () => {
|
|
146
|
+
try {
|
|
147
|
+
(await entry.scriptManagerPromise).close();
|
|
148
|
+
} catch {}
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
teardown();
|
|
153
|
+
} catch {}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function detachRealm(realmInfo: RealmInfo): Promise<void> {
|
|
158
|
+
const { realmId } = realmInfo;
|
|
159
|
+
|
|
160
|
+
const realmContext = activeRealms.get(realmId);
|
|
161
|
+
|
|
162
|
+
if (!realmContext) {
|
|
163
|
+
throw new Error(`Realm "${realmId}" has already detached`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(
|
|
167
|
+
`[${shortenRealmId(realmId)}] Detach "${realmInfo.realmType}" realm of browsing context "${realmInfo.browsingContext}" at ${realmInfo.origin}`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
realmContext.abort();
|
|
171
|
+
|
|
172
|
+
activeRealms.delete(realmId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function reconcileRealms(): Promise<void> {
|
|
176
|
+
const realms = (await scriptManager.getAllRealms()) as readonly RealmInfo[];
|
|
177
|
+
|
|
178
|
+
const realmMap = new Map<string, RealmInfo>(realms.map(realm => [realm.realmId, realm]));
|
|
179
|
+
|
|
180
|
+
const [added, _, deleted] = delta<string>(new Set(realmMap.keys()), new Set(activeRealms.keys()));
|
|
181
|
+
|
|
182
|
+
for (const realmId of deleted.values()) {
|
|
183
|
+
await detachRealm(activeRealms.get(realmId)!.realmInfo);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const realmId of added.values()) {
|
|
187
|
+
await attachRealm(realmMap.get(realmId)!);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const activeRealms: Map<string, ActiveRealmContext> = new Map();
|
|
192
|
+
|
|
193
|
+
// @types/selenium-webdriver@4.35.0 does not match selenium-webdriver@4.44.0
|
|
194
|
+
const scriptManager = (await getScriptManagerInstance(
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
null as any,
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
webDriver as any
|
|
199
|
+
)) as unknown as ScriptManager;
|
|
200
|
+
|
|
201
|
+
const sequenceReconcileRealmsCall = createSequencer();
|
|
202
|
+
|
|
203
|
+
const reconcileRealmsInSequence: typeof reconcileRealms = (...args) => {
|
|
204
|
+
return sequenceReconcileRealmsCall(async () => {
|
|
205
|
+
try {
|
|
206
|
+
return await reconcileRealms(...args);
|
|
207
|
+
} catch {}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await reconcileRealmsInSequence();
|
|
212
|
+
|
|
213
|
+
await scriptManager.onRealmCreated(() => void reconcileRealmsInSequence());
|
|
214
|
+
await scriptManager.onRealmDestroyed(() => void reconcileRealmsInSequence());
|
|
215
|
+
|
|
216
|
+
const [url] = program.args;
|
|
217
|
+
|
|
218
|
+
url && (await webDriver.navigate().to(url));
|
|
219
|
+
|
|
220
|
+
for (;;) {
|
|
221
|
+
try {
|
|
222
|
+
// Detects when user closed the browser manually.
|
|
223
|
+
await webDriver.getAllWindowHandles();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof SeleniumWebDriverError.NoSuchSessionError) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// WebDriver.getAllWindowHandles() is not event-driven, we need to call it once every second or so.
|
|
233
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// We cannot use SIGINT to shutdown browsers automatically.
|
|
237
|
+
// When WSL2 is running chromedriver.exe (on Windows):
|
|
238
|
+
//
|
|
239
|
+
// 1. SIGINT will terminate chromedriver.exe (on Windows) immediately, seems behavior from WSL2
|
|
240
|
+
// - Node.js seems intercepted SIGINT but chromedriver.exe is still being terminated
|
|
241
|
+
// 2. Browser still open because chromedriver don't close child browser processes (probably detached)
|
|
242
|
+
// 3. We lost chromedriver.exe and has no way to delete the session
|
|
243
|
+
//
|
|
244
|
+
// However, for Linux binary of chromedriver, it works. Maybe it is about how WSL2 terminate Windows-side child processes.
|
|
245
|
+
|
|
246
|
+
console.log('Shutting down');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await scriptManager.close();
|
|
250
|
+
} catch {}
|
|
251
|
+
|
|
252
|
+
for (const realmContext of activeRealms.values()) {
|
|
253
|
+
realmContext.abort();
|
|
254
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Browser, Builder } from 'selenium-webdriver';
|
|
2
|
+
import { Options as ChromeOptions, ServiceBuilder as ChromeServiceBuilder } from 'selenium-webdriver/chrome.js';
|
|
3
|
+
import { Options as EdgeOptions, ServiceBuilder as EdgeServiceBuilder } from 'selenium-webdriver/edge.js';
|
|
4
|
+
import { Options as FirefoxOptions, ServiceBuilder as FirefoxServiceBuilder } from 'selenium-webdriver/firefox.js';
|
|
5
|
+
import { Options as SafariOptions, ServiceBuilder as SafariServiceBuilder } from 'selenium-webdriver/safari.js';
|
|
6
|
+
import findChromeDriverBin from './findChromeDriverBin.ts';
|
|
7
|
+
import findEdgeDriverBin from './findEdgeDriverBin.ts';
|
|
8
|
+
import findGeckoDriverBin from './findGeckoDriverBin.ts';
|
|
9
|
+
import findHostIP from './findHostIP.ts';
|
|
10
|
+
import findLocalIP from './findLocalIP.ts';
|
|
11
|
+
import findSafariDriverBin from './findSafariDriverBin.ts';
|
|
12
|
+
|
|
13
|
+
export default async function buildWebDriver(
|
|
14
|
+
browser: 'chrome' | 'edge' | 'firefox' | 'safari',
|
|
15
|
+
{
|
|
16
|
+
pipeStdio,
|
|
17
|
+
useWindowsBinary
|
|
18
|
+
}: {
|
|
19
|
+
readonly pipeStdio: boolean;
|
|
20
|
+
readonly useWindowsBinary: boolean;
|
|
21
|
+
}
|
|
22
|
+
) {
|
|
23
|
+
const hostIP = useWindowsBinary ? await findHostIP() : '127.0.0.1';
|
|
24
|
+
const localIP = useWindowsBinary ? await findLocalIP() : '127.0.0.1';
|
|
25
|
+
|
|
26
|
+
switch (browser) {
|
|
27
|
+
case 'edge': {
|
|
28
|
+
const builder = new EdgeServiceBuilder(await findEdgeDriverBin({ windows: useWindowsBinary }))
|
|
29
|
+
// WSL2: Despite ChromeDriver hosted on same subnet, local IP must be explicitly allowed.
|
|
30
|
+
.addArguments('--allowed-ips', localIP)
|
|
31
|
+
.setHostname(hostIP);
|
|
32
|
+
|
|
33
|
+
pipeStdio && builder.setStdio([0, 1, 2]);
|
|
34
|
+
|
|
35
|
+
const options = new EdgeOptions();
|
|
36
|
+
|
|
37
|
+
options.enableBidi();
|
|
38
|
+
|
|
39
|
+
return await new Builder()
|
|
40
|
+
.forBrowser(Browser.EDGE)
|
|
41
|
+
.setEdgeOptions(options)
|
|
42
|
+
.usingServer(await builder.build().start())
|
|
43
|
+
.build();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case 'firefox': {
|
|
47
|
+
const builder = new FirefoxServiceBuilder(await findGeckoDriverBin({ windows: useWindowsBinary }))
|
|
48
|
+
// WSL2: Firefox currently has a bug that it does not use host for the `webSocketUrl`, https://github.com/mozilla/geckodriver/issues/2249.
|
|
49
|
+
.addArguments('--host', hostIP)
|
|
50
|
+
.setHostname(hostIP);
|
|
51
|
+
|
|
52
|
+
pipeStdio && builder.setStdio([0, 1, 2]);
|
|
53
|
+
|
|
54
|
+
const options = new FirefoxOptions();
|
|
55
|
+
|
|
56
|
+
options.enableBidi();
|
|
57
|
+
|
|
58
|
+
const serverURL = await builder.build().start();
|
|
59
|
+
|
|
60
|
+
return await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(options).usingServer(serverURL).build();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'safari': {
|
|
64
|
+
const builder = new SafariServiceBuilder(await findSafariDriverBin())
|
|
65
|
+
.addArguments('--host', hostIP)
|
|
66
|
+
.setHostname(hostIP);
|
|
67
|
+
|
|
68
|
+
pipeStdio && builder.setStdio([0, 1, 2]);
|
|
69
|
+
|
|
70
|
+
const options = new SafariOptions();
|
|
71
|
+
|
|
72
|
+
'enableBidi' in options && typeof options.enableBidi === 'function' && options.enableBidi();
|
|
73
|
+
|
|
74
|
+
const serverURL = await builder.build().start();
|
|
75
|
+
|
|
76
|
+
return await new Builder().forBrowser(Browser.SAFARI).setSafariOptions(options).usingServer(serverURL).build();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
default: {
|
|
80
|
+
browser satisfies 'chrome';
|
|
81
|
+
|
|
82
|
+
const builder = new ChromeServiceBuilder(await findChromeDriverBin({ windows: useWindowsBinary }))
|
|
83
|
+
// WSL2: Despite ChromeDriver hosted on same subnet, local IP must be explicitly allowed.
|
|
84
|
+
.addArguments('--allowed-ips', localIP)
|
|
85
|
+
.setHostname(hostIP);
|
|
86
|
+
|
|
87
|
+
pipeStdio && builder.setStdio([0, 1, 2]);
|
|
88
|
+
|
|
89
|
+
const options = new ChromeOptions();
|
|
90
|
+
|
|
91
|
+
options.enableBidi();
|
|
92
|
+
|
|
93
|
+
return await new Builder()
|
|
94
|
+
.forBrowser(Browser.CHROME)
|
|
95
|
+
.setChromeOptions(options)
|
|
96
|
+
.usingServer(await builder.build().start())
|
|
97
|
+
.build();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type Fn<T = unknown> = () => Promise<T>;
|
|
2
|
+
|
|
3
|
+
// We could use `p-queue` and set `{ concurrency: 1 }` but it has too many features and too big.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a sequencer which sequence function calls with concurrency of 1.
|
|
7
|
+
*
|
|
8
|
+
* @returns A function, when called, will queue the passing function into the sequencer.
|
|
9
|
+
*/
|
|
10
|
+
function createSequencer(): <T>(fn: Fn<T>) => Promise<T> {
|
|
11
|
+
const backlog: Fn<void>[] = [];
|
|
12
|
+
let isBusy = false;
|
|
13
|
+
|
|
14
|
+
async function kickoff() {
|
|
15
|
+
if (isBusy) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isBusy = true;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let fn: Fn<void> | undefined;
|
|
23
|
+
|
|
24
|
+
while ((fn = backlog.shift())) {
|
|
25
|
+
await fn();
|
|
26
|
+
}
|
|
27
|
+
} finally {
|
|
28
|
+
isBusy = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return <T>(fn: Fn<T>): Promise<T> =>
|
|
33
|
+
new Promise<T>((resolve, reject) => {
|
|
34
|
+
backlog.push(async () => {
|
|
35
|
+
try {
|
|
36
|
+
resolve(await fn());
|
|
37
|
+
} catch (error) {
|
|
38
|
+
reject(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
kickoff();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default createSequencer;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function delta<T>(left: Set<T>, right: Set<T>): readonly [Set<T>, Set<T>, Set<T>] {
|
|
2
|
+
const common = left.intersection(right);
|
|
3
|
+
const leftOnly = left.difference(common);
|
|
4
|
+
const rightOnly = right.difference(common);
|
|
5
|
+
|
|
6
|
+
if (leftOnly.size + common.size !== left.size) {
|
|
7
|
+
throw new Error('Internal error: wrong leftOnly or common');
|
|
8
|
+
} else if (rightOnly.size + common.size !== right.size) {
|
|
9
|
+
throw new Error('Internal error: wrong rightOnly or common');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return Object.freeze([leftOnly, common, rightOnly]);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { findUp } from 'find-up';
|
|
2
|
+
|
|
3
|
+
async function findChromeDriverBin_({ windows }: { readonly windows?: boolean | undefined }): Promise<string> {
|
|
4
|
+
const path = windows
|
|
5
|
+
? await findUp('chromedriver.exe')
|
|
6
|
+
: (await findUp('chromedriver')) || (await findUp('/usr/bin/chromedriver'));
|
|
7
|
+
|
|
8
|
+
if (!path) {
|
|
9
|
+
throw new Error('ChromeDriver is not found under the current path up to the root, please download ChromeDriver');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let findChromeDriverBinResult: Promise<string>;
|
|
16
|
+
|
|
17
|
+
export default function findChromeDriverBin({
|
|
18
|
+
windows
|
|
19
|
+
}: { readonly windows?: boolean | undefined } = {}): Promise<string> {
|
|
20
|
+
return findChromeDriverBinResult || (findChromeDriverBinResult = findChromeDriverBin_({ windows }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { findUp } from 'find-up';
|
|
2
|
+
|
|
3
|
+
async function findEdgeDriverBin_({ windows }: { readonly windows?: boolean | undefined }): Promise<string> {
|
|
4
|
+
const path = windows
|
|
5
|
+
? await findUp('msedgedriver.exe')
|
|
6
|
+
: (await findUp('msedgedriver')) || (await findUp('/usr/bin/msedgedriver'));
|
|
7
|
+
|
|
8
|
+
if (!path) {
|
|
9
|
+
throw new Error('EdgeDriver is not found under the current path up to the root, please download EdgeDriver');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let findEdgeDriverBinResult: Promise<string>;
|
|
16
|
+
|
|
17
|
+
export default function findEdgeDriverBin({
|
|
18
|
+
windows
|
|
19
|
+
}: { readonly windows?: boolean | undefined } = {}): Promise<string> {
|
|
20
|
+
return findEdgeDriverBinResult || (findEdgeDriverBinResult = findEdgeDriverBin_({ windows }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { findUp } from 'find-up';
|
|
2
|
+
|
|
3
|
+
async function findGeckoDriverBin_({ windows }: { readonly windows?: boolean | undefined }): Promise<string> {
|
|
4
|
+
const path = windows
|
|
5
|
+
? await findUp('geckodriver.exe')
|
|
6
|
+
: (await findUp('geckodriver')) || (await findUp('/usr/bin/geckodriver'));
|
|
7
|
+
|
|
8
|
+
if (!path) {
|
|
9
|
+
throw new Error('GeckoDriver is not found under the current path up to the root, please download GeckoDriver');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let findGeckoDriverBinResult: Promise<string>;
|
|
16
|
+
|
|
17
|
+
export default function findGeckoDriverBin({
|
|
18
|
+
windows
|
|
19
|
+
}: { readonly windows?: boolean | undefined } = {}): Promise<string> {
|
|
20
|
+
return findGeckoDriverBinResult || (findGeckoDriverBinResult = findGeckoDriverBin_({ windows }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export default async function findHostIP(): Promise<string> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
exec('ip route show default', (error, stdout) => {
|
|
8
|
+
if (error) {
|
|
9
|
+
return reject(error);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
resolve(stdout.split(' ')?.[2] || '127.0.0.1');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export default async function findLocalIP(): Promise<string> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
exec('hostname -I', (error, stdout) => {
|
|
8
|
+
if (error) {
|
|
9
|
+
return reject(error);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
resolve(stdout.split(' ')?.[0] || '127.0.0.1');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { findUp } from 'find-up';
|
|
2
|
+
|
|
3
|
+
async function findSafariDriverBin_(): Promise<string> {
|
|
4
|
+
const path = (await findUp('safaridriver')) || (await findUp('/usr/bin/safaridriver'));
|
|
5
|
+
|
|
6
|
+
if (!path) {
|
|
7
|
+
throw new Error('SafariDriver is not found under the current path up to the root, please download SafariDriver');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let findSafariDriverBinResult: Promise<string>;
|
|
14
|
+
|
|
15
|
+
export default function findSafariDriverBin(): Promise<string> {
|
|
16
|
+
return findSafariDriverBinResult || (findSafariDriverBinResult = findSafariDriverBin_());
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
// https://docs.microsoft.com/en-us/windows/wsl/compare-versions#accessing-windows-networking-apps-from-linux-host-ip
|
|
6
|
+
async function isWSL2_(): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
const procVersion = await readFile('/proc/version', 'utf-8');
|
|
9
|
+
|
|
10
|
+
return /WSL2/iu.test(procVersion);
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let promise: Promise<boolean>;
|
|
17
|
+
|
|
18
|
+
export default function isWSL2(): Promise<boolean> {
|
|
19
|
+
return promise || (promise = isWSL2_());
|
|
20
|
+
}
|
package/dist/browser.mjs
DELETED
package/dist/browser.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["console.log('Hello, World!');\n"],"mappings":";AAAA,QAAQ,IAAI,eAAe;","names":[]}
|
|
File without changes
|