@vaishnavkm/flutterbridge 0.1.0 → 0.1.1
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/README.md +9 -6
- package/index.js +137 -28
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,26 +10,26 @@ Bridge your Flutter code to your phone instantly. No USB cables. No complex ADB
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# Using npm
|
|
13
|
-
npm install -g flutterbridge
|
|
13
|
+
npm install -g @vaishnavkm/flutterbridge
|
|
14
14
|
|
|
15
15
|
# Using pnpm
|
|
16
|
-
pnpm add -g flutterbridge
|
|
16
|
+
pnpm add -g @vaishnavkm/flutterbridge
|
|
17
17
|
|
|
18
18
|
# Using bun
|
|
19
|
-
bun add -g flutterbridge
|
|
19
|
+
bun add -g @vaishnavkm/flutterbridge
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
### One-time Use (No Installation)
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Using npm
|
|
26
|
-
npx flutterbridge
|
|
26
|
+
npx @vaishnavkm/flutterbridge
|
|
27
27
|
|
|
28
28
|
# Using pnpm
|
|
29
|
-
pnpm dlx flutterbridge
|
|
29
|
+
pnpm dlx @vaishnavkm/flutterbridge
|
|
30
30
|
|
|
31
31
|
# Using bun
|
|
32
|
-
bunx flutterbridge
|
|
32
|
+
bunx @vaishnavkm/flutterbridge
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Usage
|
|
@@ -42,6 +42,9 @@ flutterbridge
|
|
|
42
42
|
|
|
43
43
|
A QR code will appear in your terminal. Scan it with the FlutterBridge companion app to connect.
|
|
44
44
|
|
|
45
|
+
If the VM service is bound to localhost, FlutterBridge will start a LAN proxy
|
|
46
|
+
and encode that address in the QR so phones can connect over WiFi.
|
|
47
|
+
|
|
45
48
|
## CLI Options
|
|
46
49
|
|
|
47
50
|
```bash
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const readline = require('readline');
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
+
const net = require('net');
|
|
10
11
|
|
|
11
12
|
const VM_URL_TIMEOUT_MS = 60000;
|
|
12
13
|
const SERVICE_URI_KEYS = [
|
|
@@ -18,6 +19,8 @@ const SERVICE_URI_KEYS = [
|
|
|
18
19
|
'wsUri',
|
|
19
20
|
];
|
|
20
21
|
|
|
22
|
+
const activeProxies = new Set();
|
|
23
|
+
|
|
21
24
|
function parseArgs(argv) {
|
|
22
25
|
let deviceId = null;
|
|
23
26
|
let qrOnly = false;
|
|
@@ -169,25 +172,103 @@ function getLanIp() {
|
|
|
169
172
|
return candidates[0];
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
function getDefaultPort(protocol) {
|
|
176
|
+
if (protocol === 'wss:' || protocol === 'https:') {
|
|
177
|
+
return 443;
|
|
178
|
+
}
|
|
179
|
+
if (protocol === 'ws:' || protocol === 'http:') {
|
|
180
|
+
return 80;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function registerProxy(server) {
|
|
186
|
+
activeProxies.add(server);
|
|
187
|
+
server.on('close', () => activeProxies.delete(server));
|
|
188
|
+
}
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
function closeAllProxies() {
|
|
191
|
+
for (const server of activeProxies) {
|
|
192
|
+
try {
|
|
193
|
+
server.close();
|
|
194
|
+
} catch (_) {
|
|
195
|
+
// Ignore errors while shutting down.
|
|
185
196
|
}
|
|
197
|
+
}
|
|
198
|
+
activeProxies.clear();
|
|
199
|
+
}
|
|
186
200
|
|
|
187
|
-
|
|
188
|
-
|
|
201
|
+
process.on('exit', closeAllProxies);
|
|
202
|
+
|
|
203
|
+
function startTcpProxy(targetHost, targetPort) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const server = net.createServer((client) => {
|
|
206
|
+
const upstream = net.connect({ host: targetHost, port: targetPort });
|
|
207
|
+
|
|
208
|
+
const closeBoth = () => {
|
|
209
|
+
client.destroy();
|
|
210
|
+
upstream.destroy();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
client.on('error', closeBoth);
|
|
214
|
+
upstream.on('error', closeBoth);
|
|
215
|
+
|
|
216
|
+
client.pipe(upstream);
|
|
217
|
+
upstream.pipe(client);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
server.on('error', reject);
|
|
221
|
+
|
|
222
|
+
server.listen(0, '0.0.0.0', () => {
|
|
223
|
+
registerProxy(server);
|
|
224
|
+
const address = server.address();
|
|
225
|
+
resolve({ server, port: address.port });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function prepareVmServiceUrl(originalUrl) {
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = new URL(originalUrl);
|
|
189
234
|
} catch (_) {
|
|
190
|
-
return { url: originalUrl, replaced: false };
|
|
235
|
+
return { url: originalUrl, originalUrl, replaced: false, proxied: false };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!isLoopbackHost(parsed.hostname)) {
|
|
239
|
+
return { url: originalUrl, originalUrl, replaced: false, proxied: false };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lanIp = getLanIp();
|
|
243
|
+
if (!lanIp) {
|
|
244
|
+
console.warn(chalk.yellow('\n⚠️ Warning: Could not detect LAN IP address.'));
|
|
245
|
+
console.warn(chalk.yellow('Make sure your PC and phone are on the same WiFi network.'));
|
|
246
|
+
console.warn(chalk.yellow('Connection may fail with localhost URL.\n'));
|
|
247
|
+
return { url: originalUrl, originalUrl, replaced: false, proxied: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const port = parsed.port ? Number(parsed.port) : getDefaultPort(parsed.protocol);
|
|
251
|
+
if (!port) {
|
|
252
|
+
return { url: originalUrl, originalUrl, replaced: false, proxied: false };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const targetHost = parsed.hostname === '0.0.0.0' ? '127.0.0.1' : parsed.hostname;
|
|
256
|
+
try {
|
|
257
|
+
const proxy = await startTcpProxy(targetHost, port);
|
|
258
|
+
parsed.hostname = lanIp;
|
|
259
|
+
parsed.port = String(proxy.port);
|
|
260
|
+
return {
|
|
261
|
+
url: parsed.toString(),
|
|
262
|
+
originalUrl,
|
|
263
|
+
replaced: true,
|
|
264
|
+
proxied: true,
|
|
265
|
+
proxyHost: lanIp,
|
|
266
|
+
proxyPort: proxy.port,
|
|
267
|
+
};
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.warn(chalk.yellow(`\n⚠️ Warning: Failed to start LAN proxy (${err.message}).`));
|
|
270
|
+
parsed.hostname = lanIp;
|
|
271
|
+
return { url: parsed.toString(), originalUrl, replaced: true, proxied: false };
|
|
191
272
|
}
|
|
192
273
|
}
|
|
193
274
|
|
|
@@ -395,6 +476,7 @@ async function main() {
|
|
|
395
476
|
let stdoutBuffer = '';
|
|
396
477
|
let stderrBuffer = '';
|
|
397
478
|
let vmServiceUrl = null;
|
|
479
|
+
let publishPending = false;
|
|
398
480
|
|
|
399
481
|
const vmTimeout = setTimeout(() => {
|
|
400
482
|
if (!vmServiceUrl) {
|
|
@@ -420,29 +502,56 @@ async function main() {
|
|
|
420
502
|
const events = Array.isArray(json) ? json : [json];
|
|
421
503
|
for (const event of events) {
|
|
422
504
|
const url = extractVmServiceUri(event);
|
|
423
|
-
if (url && !vmServiceUrl) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (rewritten.replaced) {
|
|
430
|
-
payload.originalVmServiceUri = url;
|
|
505
|
+
if (url && !vmServiceUrl && !publishPending) {
|
|
506
|
+
publishPending = true;
|
|
507
|
+
|
|
508
|
+
const publishUrl = (result) => {
|
|
509
|
+
if (vmServiceUrl) {
|
|
510
|
+
return;
|
|
431
511
|
}
|
|
432
|
-
|
|
433
|
-
|
|
512
|
+
vmServiceUrl = result.url;
|
|
513
|
+
clearTimeout(vmTimeout);
|
|
514
|
+
|
|
515
|
+
if (jsonOutput) {
|
|
516
|
+
const payload = { vmServiceUri: vmServiceUrl, deviceId };
|
|
517
|
+
if (result.replaced) {
|
|
518
|
+
payload.originalVmServiceUri = result.originalUrl;
|
|
519
|
+
}
|
|
520
|
+
if (result.proxied) {
|
|
521
|
+
payload.proxy = { host: result.proxyHost, port: result.proxyPort };
|
|
522
|
+
}
|
|
523
|
+
console.log(JSON.stringify(payload));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
434
527
|
if (!qrOnly) {
|
|
435
528
|
console.log(chalk.yellow('\nScan this QR with FlutterBridge app:\n'));
|
|
436
529
|
}
|
|
437
530
|
qrcode.generate(vmServiceUrl, { small: true });
|
|
438
531
|
if (!qrOnly) {
|
|
439
|
-
if (
|
|
532
|
+
if (result.proxied) {
|
|
533
|
+
console.log(chalk.gray(`LAN proxy running at ws://${result.proxyHost}:${result.proxyPort}`));
|
|
534
|
+
console.log(chalk.gray(`Proxy target: ${result.originalUrl}`));
|
|
535
|
+
} else if (result.replaced) {
|
|
440
536
|
console.log(chalk.gray(`Rewrote VM URL for LAN access: ${vmServiceUrl}`));
|
|
441
|
-
console.log(chalk.gray(`Original VM URL: ${
|
|
537
|
+
console.log(chalk.gray(`Original VM URL: ${result.originalUrl}`));
|
|
442
538
|
}
|
|
443
539
|
console.log(chalk.green(`\nVM URL: ${vmServiceUrl}`));
|
|
444
540
|
}
|
|
445
|
-
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
prepareVmServiceUrl(url)
|
|
544
|
+
.then((result) => {
|
|
545
|
+
publishPending = false;
|
|
546
|
+
publishUrl(result);
|
|
547
|
+
})
|
|
548
|
+
.catch((err) => {
|
|
549
|
+
publishPending = false;
|
|
550
|
+
if (!quiet) {
|
|
551
|
+
console.error(chalk.red(`Failed to prepare VM service URL: ${err.message}`));
|
|
552
|
+
}
|
|
553
|
+
publishUrl({ url, originalUrl: url, replaced: false, proxied: false });
|
|
554
|
+
});
|
|
446
555
|
}
|
|
447
556
|
}
|
|
448
557
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaishnavkm/flutterbridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Bridge your Flutter code to your phone instantly. Wireless development with QR code pairing.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"flutterbridge": "
|
|
7
|
+
"flutterbridge": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/vaishnavkm/flutterbridge.git"
|
|
31
|
+
"url": "git+https://github.com/vaishnavkm/flutterbridge.git"
|
|
32
32
|
},
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/vaishnavkm/flutterbridge/issues"
|