@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/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.0.0-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
+ }
@@ -0,0 +1,7 @@
1
+ export default function shortenRealmId(value: string): string {
2
+ if (value.length > 8) {
3
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
4
+ }
5
+
6
+ return value;
7
+ }
package/dist/browser.mjs DELETED
@@ -1,3 +0,0 @@
1
- // src/index.ts
2
- console.log("Hello, World!");
3
- //# sourceMappingURL=browser.mjs.map
@@ -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