Skip to content

Commit c6fd01e

Browse files
committed
refactor: split code
1 parent 9926d92 commit c6fd01e

File tree

5 files changed

+321
-299
lines changed

5 files changed

+321
-299
lines changed

src/_utils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { promises as fs } from "node:fs";
2+
import { networkInterfaces } from "node:os";
3+
import { cyan, underline, bold } from "colorette";
4+
import type { Certificate, HTTPSOptions } from "./types";
5+
6+
export async function resolveCert(
7+
options: HTTPSOptions,
8+
host?: string,
9+
): Promise<Certificate> {
10+
// Use cert if provided
11+
if (options.key && options.cert) {
12+
const isInline = (s = "") => s.startsWith("--");
13+
const r = (s: string) => (isInline(s) ? s : fs.readFile(s, "utf8"));
14+
return {
15+
key: await r(options.key),
16+
cert: await r(options.cert),
17+
};
18+
}
19+
20+
// Use auto generated cert
21+
const { generateCA, generateSSLCert } = await import("./cert");
22+
const ca = await generateCA();
23+
const cert = await generateSSLCert({
24+
caCert: ca.cert,
25+
caKey: ca.key,
26+
domains:
27+
options.domains ||
28+
(["localhost", "127.0.0.1", "::1", host].filter(Boolean) as string[]),
29+
validityDays: options.validityDays || 1,
30+
});
31+
return cert;
32+
}
33+
34+
export function getNetworkInterfaces(v4Only = true): string[] {
35+
const addrs = new Set<string>();
36+
for (const details of Object.values(networkInterfaces())) {
37+
if (details) {
38+
for (const d of details) {
39+
if (
40+
!d.internal &&
41+
!(d.mac === "00:00:00:00:00:00") &&
42+
!d.address.startsWith("fe80::") &&
43+
!(v4Only && (d.family === "IPv6" || +d.family === 6))
44+
) {
45+
addrs.add(formatAddress(d));
46+
}
47+
}
48+
}
49+
}
50+
return [...addrs].sort();
51+
}
52+
53+
export function formatAddress(addr: {
54+
family: string | number;
55+
address: string;
56+
}) {
57+
return addr.family === "IPv6" || addr.family === 6
58+
? `[${addr.address}]`
59+
: addr.address;
60+
}
61+
62+
export function formatURL(url: string) {
63+
return cyan(
64+
underline(decodeURI(url).replace(/:(\d+)\//g, `:${bold("$1")}/`)),
65+
);
66+
}

src/index.ts

Lines changed: 3 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -1,299 +1,3 @@
1-
import { RequestListener, Server, createServer } from "node:http";
2-
import {
3-
Server as HTTPServer,
4-
createServer as createHTTPSServer,
5-
} from "node:https";
6-
import { promisify } from "node:util";
7-
import { resolve } from "node:path";
8-
import { promises as fs, watch } from "node:fs";
9-
import { networkInterfaces } from "node:os";
10-
import type { AddressInfo } from "node:net";
11-
import { fileURLToPath } from "mlly";
12-
import { cyan, gray, underline, bold } from "colorette";
13-
import { getPort, GetPortInput } from "get-port-please";
14-
import addShutdown from "http-shutdown";
15-
import { defu } from "defu";
16-
import { open } from "./lib/open";
17-
18-
export interface Certificate {
19-
key: string;
20-
cert: string;
21-
}
22-
23-
export interface HTTPSOptions {
24-
cert: string;
25-
key: string;
26-
domains?: string[];
27-
validityDays?: number;
28-
}
29-
30-
export interface ListenOptions {
31-
name: string;
32-
port?: GetPortInput;
33-
hostname: string;
34-
showURL: boolean;
35-
baseURL: string;
36-
open: boolean;
37-
https: boolean | HTTPSOptions;
38-
clipboard: boolean;
39-
isTest: boolean;
40-
isProd: boolean;
41-
autoClose: boolean;
42-
autoCloseSignals: string[];
43-
}
44-
45-
export interface WatchOptions {
46-
cwd: string;
47-
entry: string;
48-
}
49-
50-
export interface ShowURLOptions {
51-
baseURL: string;
52-
name?: string;
53-
}
54-
55-
export interface Listener {
56-
url: string;
57-
address: any;
58-
server: Server | HTTPServer;
59-
https: false | Certificate;
60-
close: () => Promise<void>;
61-
open: () => Promise<void>;
62-
showURL: (options?: Pick<ListenOptions, "baseURL">) => void;
63-
}
64-
65-
export async function listen(
66-
handle: RequestListener,
67-
options_: Partial<ListenOptions> = {},
68-
): Promise<Listener> {
69-
options_ = defu(options_, {
70-
port: process.env.PORT || 3000,
71-
hostname: process.env.HOST || "",
72-
showURL: true,
73-
baseURL: "/",
74-
open: false,
75-
clipboard: false,
76-
isTest: process.env.NODE_ENV === "test",
77-
isProd: process.env.NODE_ENV === "production",
78-
autoClose: true,
79-
});
80-
81-
if (options_.isTest) {
82-
options_.showURL = false;
83-
}
84-
85-
if (options_.isProd || options_.isTest) {
86-
options_.open = false;
87-
options_.clipboard = false;
88-
}
89-
90-
const port = await getPort({
91-
port: Number(options_.port),
92-
verbose: !options_.isTest,
93-
host: options_.hostname,
94-
alternativePortRange: [3000, 3100],
95-
...(typeof options_.port === "object" && options_.port),
96-
});
97-
98-
let server: Server | HTTPServer;
99-
100-
let addr: { proto: "http" | "https"; addr: string; port: number } | null;
101-
const getURL = (host?: string, baseURL?: string) => {
102-
const anyV4 = addr?.addr === "0.0.0.0";
103-
const anyV6 = addr?.addr === "[::]";
104-
105-
return `${addr!.proto}://${
106-
host || options_.hostname || (anyV4 || anyV6 ? "localhost" : addr!.addr)
107-
}:${addr!.port}${baseURL || options_.baseURL}`;
108-
};
109-
110-
let https: Listener["https"] = false;
111-
if (options_.https) {
112-
const { key, cert } = await resolveCert(
113-
{ ...(options_.https as any) },
114-
options_.hostname,
115-
);
116-
https = { key, cert };
117-
server = createHTTPSServer({ key, cert }, handle);
118-
addShutdown(server);
119-
// @ts-ignore
120-
await promisify(server.listen.bind(server))(port, options_.hostname);
121-
const _addr = server.address() as AddressInfo;
122-
addr = { proto: "https", addr: formatAddress(_addr), port: _addr.port };
123-
} else {
124-
server = createServer(handle);
125-
addShutdown(server);
126-
// @ts-ignore
127-
await promisify(server.listen.bind(server))(port, options_.hostname);
128-
const _addr = server.address() as AddressInfo;
129-
addr = { proto: "http", addr: formatAddress(_addr), port: _addr.port };
130-
}
131-
132-
let _closed = false;
133-
const close = () => {
134-
if (_closed) {
135-
return Promise.resolve();
136-
}
137-
_closed = true;
138-
return promisify((server as any).shutdown)();
139-
};
140-
141-
if (options_.clipboard) {
142-
const clipboardy = await import("clipboardy").then((r) => r.default || r);
143-
await clipboardy.write(getURL()).catch(() => {
144-
options_.clipboard = false;
145-
});
146-
}
147-
148-
const showURL = (options?: ShowURLOptions) => {
149-
const add = options_.clipboard ? gray("(copied to clipboard)") : "";
150-
const lines = [];
151-
const baseURL = options?.baseURL || options_.baseURL || "";
152-
const name = options?.name ? ` (${options.name})` : "";
153-
154-
const anyV4 = addr?.addr === "0.0.0.0";
155-
const anyV6 = addr?.addr === "[::]";
156-
if (anyV4 || anyV6) {
157-
lines.push(
158-
` > Local${name}: ${formatURL(
159-
getURL("localhost", baseURL),
160-
)} ${add}`,
161-
);
162-
for (const addr of getNetworkInterfaces(anyV4)) {
163-
lines.push(` > Network${name}: ${formatURL(getURL(addr, baseURL))}`);
164-
}
165-
} else {
166-
lines.push(
167-
` > Listening${name}: ${formatURL(
168-
getURL(undefined, baseURL),
169-
)} ${add}`,
170-
);
171-
}
172-
// eslint-disable-next-line no-console
173-
console.log("\n" + lines.join("\n") + "\n");
174-
};
175-
176-
if (options_.showURL) {
177-
showURL();
178-
}
179-
180-
const _open = async () => {
181-
await open(getURL()).catch(() => {});
182-
};
183-
if (options_.open) {
184-
await _open();
185-
}
186-
187-
if (options_.autoClose) {
188-
process.on("exit", () => close());
189-
}
190-
191-
return <Listener>{
192-
url: getURL(),
193-
https,
194-
server,
195-
open: _open,
196-
showURL,
197-
close,
198-
};
199-
}
200-
201-
export async function listenAndWatch(
202-
input: string,
203-
options: Partial<ListenOptions & WatchOptions> = {},
204-
): Promise<Listener> {
205-
const cwd = resolve(options.cwd ? fileURLToPath(options.cwd) : ".");
206-
207-
const jiti = await import("jiti").then((r) => r.default || r);
208-
const _jitiRequire = jiti(cwd, {
209-
esmResolve: true,
210-
requireCache: false,
211-
interopDefault: true,
212-
});
213-
214-
const entry = _jitiRequire.resolve(input);
215-
216-
let handle: RequestListener;
217-
218-
const resolveHandle = () => {
219-
const imported = _jitiRequire(entry);
220-
handle = imported.default || imported;
221-
};
222-
223-
resolveHandle();
224-
225-
const watcher = await watch(entry, () => {
226-
resolveHandle();
227-
});
228-
229-
const listenter = await listen((...args) => {
230-
return handle(...args);
231-
}, options);
232-
233-
const _close = listenter.close;
234-
listenter.close = async () => {
235-
watcher.close();
236-
await _close();
237-
};
238-
239-
return listenter;
240-
}
241-
242-
async function resolveCert(
243-
options: HTTPSOptions,
244-
host?: string,
245-
): Promise<Certificate> {
246-
// Use cert if provided
247-
if (options.key && options.cert) {
248-
const isInline = (s = "") => s.startsWith("--");
249-
const r = (s: string) => (isInline(s) ? s : fs.readFile(s, "utf8"));
250-
return {
251-
key: await r(options.key),
252-
cert: await r(options.cert),
253-
};
254-
}
255-
256-
// Use auto generated cert
257-
const { generateCA, generateSSLCert } = await import("./cert");
258-
const ca = await generateCA();
259-
const cert = await generateSSLCert({
260-
caCert: ca.cert,
261-
caKey: ca.key,
262-
domains:
263-
options.domains ||
264-
(["localhost", "127.0.0.1", "::1", host].filter(Boolean) as string[]),
265-
validityDays: options.validityDays || 1,
266-
});
267-
return cert;
268-
}
269-
270-
function getNetworkInterfaces(v4Only = true): string[] {
271-
const addrs = new Set<string>();
272-
for (const details of Object.values(networkInterfaces())) {
273-
if (details) {
274-
for (const d of details) {
275-
if (
276-
!d.internal &&
277-
!(d.mac === "00:00:00:00:00:00") &&
278-
!d.address.startsWith("fe80::") &&
279-
!(v4Only && (d.family === "IPv6" || +d.family === 6))
280-
) {
281-
addrs.add(formatAddress(d));
282-
}
283-
}
284-
}
285-
}
286-
return [...addrs].sort();
287-
}
288-
289-
function formatAddress(addr: { family: string | number; address: string }) {
290-
return addr.family === "IPv6" || addr.family === 6
291-
? `[${addr.address}]`
292-
: addr.address;
293-
}
294-
295-
function formatURL(url: string) {
296-
return cyan(
297-
underline(decodeURI(url).replace(/:(\d+)\//g, `:${bold("$1")}/`)),
298-
);
299-
}
1+
export * from "./listen";
2+
export * from "./types";
3+
export * from "./watch";

0 commit comments

Comments
 (0)