@montytools/cli 0.2.0 → 0.2.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/bin/monty.mjs +45 -7
- package/package.json +1 -1
- package/template/package.json +1 -1
package/bin/monty.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
+
import { connect as netConnect } from "node:net";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { basename, dirname, join, relative } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -396,14 +397,25 @@ function typecheckApp() {
|
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
|
|
400
|
+
// Probe by CONNECTING (not binding): SO_REUSEADDR lets a wildcard bind
|
|
401
|
+
// "succeed" on macOS even when a specific loopback address holds the port,
|
|
402
|
+
// so bind-probes lie. A successful connect = someone is listening.
|
|
403
|
+
function portTaken(host, port) {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const sock = netConnect({ host, port });
|
|
406
|
+
const done = (taken) => {
|
|
407
|
+
sock.destroy();
|
|
408
|
+
resolve(taken);
|
|
409
|
+
};
|
|
410
|
+
sock.once("connect", () => done(true));
|
|
411
|
+
sock.once("error", () => done(false));
|
|
412
|
+
sock.setTimeout(400, () => done(false));
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
399
416
|
async function freePort(start) {
|
|
400
417
|
for (let p = start; p < start + 50; p++) {
|
|
401
|
-
|
|
402
|
-
const probe = createServer();
|
|
403
|
-
probe.once("error", () => resolve(false));
|
|
404
|
-
probe.listen(p, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
405
|
-
});
|
|
406
|
-
if (ok) return p;
|
|
418
|
+
if (!(await portTaken("127.0.0.1", p)) && !(await portTaken("::1", p))) return p;
|
|
407
419
|
}
|
|
408
420
|
fail("NO_FREE_PORT", `No free port between ${start} and ${start + 49}. Pass --port <n>.`);
|
|
409
421
|
}
|
|
@@ -499,8 +511,14 @@ async function dev() {
|
|
|
499
511
|
const t = await startTunnel(port);
|
|
500
512
|
tunnelChild = t.child;
|
|
501
513
|
if (t.url) {
|
|
502
|
-
originUrl = t.url;
|
|
503
514
|
console.log(`tunnel: ${t.url}`);
|
|
515
|
+
console.log("tunnel: waiting for DNS to go live (prevents cached failures in your browser)…");
|
|
516
|
+
if (await waitForDns(t.url.replace("https://", ""))) {
|
|
517
|
+
originUrl = t.url;
|
|
518
|
+
console.log("tunnel: DNS live");
|
|
519
|
+
} else {
|
|
520
|
+
console.log("tunnel: DNS never propagated — dev mode registered on localhost (visible on this machine's browser only)");
|
|
521
|
+
}
|
|
504
522
|
} else {
|
|
505
523
|
console.log("tunnel: unavailable — dev mode registered on localhost (visible on this machine's browser only)");
|
|
506
524
|
}
|
|
@@ -532,6 +550,26 @@ async function dev() {
|
|
|
532
550
|
});
|
|
533
551
|
}
|
|
534
552
|
|
|
553
|
+
|
|
554
|
+
// trycloudflare DNS takes up to a couple of minutes to propagate. Registering
|
|
555
|
+
// the origin before it resolves would make admins' browsers cache NXDOMAIN
|
|
556
|
+
// (macOS negative cache ≈ 30 min of a broken iframe) — so gate on DNS via
|
|
557
|
+
// DoH, which never touches the local resolver.
|
|
558
|
+
async function waitForDns(hostname) {
|
|
559
|
+
for (let i = 0; i < 36; i++) {
|
|
560
|
+
try {
|
|
561
|
+
const r = await fetch(
|
|
562
|
+
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`,
|
|
563
|
+
{ headers: { accept: "application/dns-json" } },
|
|
564
|
+
);
|
|
565
|
+
const d = await r.json();
|
|
566
|
+
if (Array.isArray(d.Answer) && d.Answer.some((a) => a.type === 1)) return true;
|
|
567
|
+
} catch { /* transient — retry */ }
|
|
568
|
+
await new Promise((res) => setTimeout(res, 5000));
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
535
573
|
// Cloudflare quick tunnel via the cloudflared npm wrapper (downloads the
|
|
536
574
|
// binary on first use). Resolves with the public URL, or null on failure —
|
|
537
575
|
// dev mode then falls back to localhost-only registration.
|
package/package.json
CHANGED
package/template/package.json
CHANGED