Skip to content

Commit cf4317c

Browse files
committed
refactor: split internals and types
1 parent 836ca3c commit cf4317c

File tree

4 files changed

+278
-260
lines changed

4 files changed

+278
-260
lines changed

src/_internal.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { createServer, AddressInfo } from "node:net";
2+
import { networkInterfaces } from "node:os";
3+
import { isSafePort } from "./unsafe-ports";
4+
import type { PortNumber, HostAddress } from "./types";
5+
6+
export class GetPortError extends Error {
7+
name = "GetPortError";
8+
constructor(
9+
public message: string,
10+
opts?: any,
11+
) {
12+
super(message, opts);
13+
}
14+
}
15+
16+
export function _log(verbose: boolean, message: string) {
17+
if (verbose) {
18+
console.log("[get-port]", message);
19+
}
20+
}
21+
22+
export function _generateRange(from: number, to: number): number[] {
23+
if (to < from) {
24+
return [];
25+
}
26+
const r = [];
27+
for (let index = from; index < to; index++) {
28+
r.push(index);
29+
}
30+
return r;
31+
}
32+
33+
export function _tryPort(
34+
port: PortNumber,
35+
host: HostAddress,
36+
): Promise<PortNumber | false> {
37+
return new Promise((resolve) => {
38+
const server = createServer();
39+
server.unref();
40+
server.on("error", (error: Error & { code: string }) => {
41+
// Ignore invalid host
42+
if (error.code === "EINVAL" || error.code === "EADDRNOTAVAIL") {
43+
resolve(port !== 0 && isSafePort(port) && port);
44+
} else {
45+
resolve(false);
46+
}
47+
});
48+
server.listen({ port, host }, () => {
49+
const { port } = server.address() as AddressInfo;
50+
server.close(() => {
51+
resolve(isSafePort(port) && port);
52+
});
53+
});
54+
});
55+
}
56+
57+
export function _getLocalHosts(additional: HostAddress[]): HostAddress[] {
58+
const hosts = new Set<HostAddress>(additional);
59+
for (const _interface of Object.values(networkInterfaces())) {
60+
for (const config of _interface || []) {
61+
hosts.add(config.address);
62+
}
63+
}
64+
return [...hosts];
65+
}
66+
67+
export async function _findPort(
68+
ports: number[],
69+
host: HostAddress,
70+
): Promise<PortNumber> {
71+
for (const port of ports) {
72+
const r = await _tryPort(port, host);
73+
if (r) {
74+
return r;
75+
}
76+
}
77+
}
78+
79+
export function _fmtOnHost(hostname: string | undefined) {
80+
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
81+
}
82+
83+
const HOSTNAME_RE = /^(?!-)[\d.A-Za-z-]{1,63}(?<!-)$/;
84+
85+
export function _validateHostname(hostname: string | undefined) {
86+
if (hostname && !HOSTNAME_RE.test(hostname)) {
87+
throw new GetPortError(`Invalid host: ${JSON.stringify(hostname)}`);
88+
}
89+
return hostname;
90+
}

src/get-port.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { isSafePort } from "./unsafe-ports";
2+
3+
import type {
4+
GetPortInput,
5+
PortNumber,
6+
GetPortOptions,
7+
HostAddress,
8+
WaitForPortOptions,
9+
} from "./types";
10+
11+
import {
12+
GetPortError,
13+
_findPort,
14+
_fmtOnHost,
15+
_generateRange,
16+
_getLocalHosts,
17+
_tryPort,
18+
_log,
19+
_validateHostname,
20+
} from "./_internal";
21+
22+
export async function getPort(
23+
_userOptions: GetPortInput = {},
24+
): Promise<PortNumber> {
25+
if (typeof _userOptions === "number" || typeof _userOptions === "string") {
26+
_userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
27+
}
28+
29+
const _port = Number(_userOptions.port ?? process.env.PORT ?? 3000);
30+
31+
const options = {
32+
name: "default",
33+
random: _port === 0,
34+
ports: [],
35+
portRange: [],
36+
alternativePortRange: _userOptions.port ? [] : [3000, 3100],
37+
verbose: false,
38+
..._userOptions,
39+
port: _port,
40+
host: _validateHostname(_userOptions.host ?? process.env.HOST),
41+
} as GetPortOptions;
42+
43+
if (options.random) {
44+
return getRandomPort(options.host);
45+
}
46+
47+
// Generate list of ports to check
48+
const portsToCheck: PortNumber[] = [
49+
options.port,
50+
...options.ports,
51+
..._generateRange(...options.portRange),
52+
].filter((port) => {
53+
if (!port) {
54+
return false;
55+
}
56+
if (!isSafePort(port)) {
57+
_log(options.verbose, `Ignoring unsafe port: ${port}`);
58+
return false;
59+
}
60+
return true;
61+
});
62+
63+
// Try to find a port
64+
let availablePort = await _findPort(portsToCheck, options.host);
65+
66+
// Try fallback port range
67+
if (!availablePort && options.alternativePortRange.length > 0) {
68+
availablePort = await _findPort(
69+
_generateRange(...options.alternativePortRange),
70+
options.host,
71+
);
72+
_log(
73+
options.verbose,
74+
`Unable to find an available port (tried ${options.alternativePortRange.join(
75+
"-",
76+
)} ${_fmtOnHost(options.host)}). Using alternative port ${availablePort}`,
77+
);
78+
}
79+
80+
// Try random port
81+
if (!availablePort && _userOptions.random !== false) {
82+
availablePort = await getRandomPort(options.host);
83+
if (availablePort) {
84+
_log(options.verbose, `Using random port ${availablePort}`);
85+
}
86+
}
87+
88+
// Throw error if no port is available
89+
if (!availablePort) {
90+
const triedRanges = [
91+
options.port,
92+
options.portRange.join("-"),
93+
options.alternativePortRange.join("-"),
94+
]
95+
.filter(Boolean)
96+
.join(", ");
97+
throw new GetPortError(
98+
`Unable to find find available port ${_fmtOnHost(
99+
options.host,
100+
)} (tried ${triedRanges})`,
101+
);
102+
}
103+
104+
return availablePort;
105+
}
106+
107+
export async function getRandomPort(host?: HostAddress) {
108+
const port = await checkPort(0, host);
109+
if (port === false) {
110+
throw new GetPortError(
111+
`Unable to find any random port ${_fmtOnHost(host)}`,
112+
);
113+
}
114+
return port;
115+
}
116+
117+
export async function waitForPort(
118+
port: PortNumber,
119+
options: WaitForPortOptions = {},
120+
) {
121+
const delay = options.delay || 500;
122+
const retries = options.retries || 4;
123+
for (let index = retries; index > 0; index--) {
124+
if ((await _tryPort(port, options.host)) === false) {
125+
return;
126+
}
127+
await new Promise((resolve) => setTimeout(resolve, delay));
128+
}
129+
throw new GetPortError(
130+
`Timeout waiting for port ${port} after ${retries} retries with ${delay}ms interval.`,
131+
);
132+
}
133+
134+
export async function checkPort(
135+
port: PortNumber,
136+
host: HostAddress | HostAddress[] = process.env.HOST,
137+
verbose?: boolean,
138+
): Promise<PortNumber | false> {
139+
if (!host) {
140+
host = _getLocalHosts([undefined /* default */, "0.0.0.0"]);
141+
}
142+
if (!Array.isArray(host)) {
143+
return _tryPort(port, host);
144+
}
145+
for (const _host of host) {
146+
const _port = await _tryPort(port, _host);
147+
if (_port === false) {
148+
if (port < 1024 && verbose) {
149+
_log(
150+
verbose,
151+
`Unable to listen to the priviliged port ${port} ${_fmtOnHost(
152+
_host,
153+
)}`,
154+
);
155+
}
156+
return false;
157+
}
158+
if (port === 0 && _port !== 0) {
159+
port = _port;
160+
}
161+
}
162+
return port;
163+
}

0 commit comments

Comments
 (0)