unplugin-cloudflare-tunnel 0.1.0 → 0.1.2

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.mjs CHANGED
@@ -1,1089 +1,2 @@
1
- import { a as number, c as string, i as nullish, l as unknown, n as array, o as object, r as boolean, s as optional, t as any } from "./schemas-Cpk3vGGi.mjs";
2
- import { createUnplugin } from "unplugin";
3
- import NodeFS from "node:fs/promises";
4
- import { bin, install } from "cloudflared";
5
- import * as NodeChildProcess from "node:child_process";
6
- import * as NodeUtil from "node:util";
7
- //#region src/index.ts
8
- /**
9
- * @fileoverview Cloudflare Tunnel Unplugin
10
- *
11
- * A cross-bundler plugin that automatically creates and manages
12
- * Cloudflare tunnels for local development, providing instant HTTPS access
13
- * to your local dev server from anywhere on the internet.
14
- *
15
- * @author Cloudflare Tunnel Plugin Contributors
16
- * @version 1.0.0
17
- * @license MIT
18
- */
19
- const PLUGIN_NAME = "unplugin-cloudflare-tunnel";
20
- const INFO_LOG_REGEX = /^.*Z INF .*/;
21
- const LOG_LEVEL_RANK = {
22
- debug: 10,
23
- info: 20,
24
- warn: 30,
25
- error: 40,
26
- fatal: 50
27
- };
28
- function shouldLog(threshold, level) {
29
- return LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[threshold];
30
- }
31
- function supportsColor() {
32
- if (!process.stdout.isTTY) return false;
33
- if (process.env.NO_COLOR !== void 0) return false;
34
- if (process.env.TERM === "dumb") return false;
35
- if (process.env.FORCE_COLOR === "0") return false;
36
- return true;
37
- }
38
- const ANSI = {
39
- reset: "\x1B[0m",
40
- dim: "\x1B[2m",
41
- bold: "\x1B[1m",
42
- blue: "\x1B[34m",
43
- yellow: "\x1B[33m"
44
- };
45
- const ANSI_STYLE_SEQUENCE_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
46
- function stripAnsi(text) {
47
- return text.replace(ANSI_STYLE_SEQUENCE_REGEX, "");
48
- }
49
- function colorize(text, ansi) {
50
- if (!supportsColor()) return text;
51
- return `${ansi}${text}${ANSI.reset}`;
52
- }
53
- const CloudflareErrorSchema = object({
54
- code: number(),
55
- message: string()
56
- });
57
- const CloudflareApiResponseSchema = object({
58
- success: boolean(),
59
- errors: optional(array(CloudflareErrorSchema)),
60
- messages: optional(array(string())),
61
- result: unknown()
62
- });
63
- const AccountSchema = object({
64
- id: string(),
65
- name: string()
66
- });
67
- const ZoneSchema = object({
68
- id: string(),
69
- name: string(),
70
- account: optional(object({ id: string() }))
71
- });
72
- const TunnelSchema = object({
73
- id: string(),
74
- name: string(),
75
- account_tag: string(),
76
- created_at: string(),
77
- connections: optional(array(unknown()))
78
- });
79
- const DNSRecordSchema = object({
80
- id: string(),
81
- type: string(),
82
- name: string(),
83
- content: string(),
84
- proxied: boolean(),
85
- comment: nullish(string())
86
- });
87
- const unpluginFactory = (options = {}) => {
88
- const { enabled = true } = options;
89
- if (enabled === false) {
90
- const VIRTUAL_MODULE_ID_STUB = "virtual:unplugin-cloudflare-tunnel";
91
- return {
92
- name: PLUGIN_NAME,
93
- enforce: "pre",
94
- resolveId(id) {
95
- if (id === VIRTUAL_MODULE_ID_STUB) return id;
96
- },
97
- loadInclude(id) {
98
- return id === VIRTUAL_MODULE_ID_STUB;
99
- },
100
- load(id) {
101
- if (id === VIRTUAL_MODULE_ID_STUB) return "export function getTunnelUrl() { return \"\"; }";
102
- }
103
- };
104
- }
105
- const GLOBAL_STATE = Symbol.for("unplugin-cloudflare-tunnel.state");
106
- const globalState = globalThis[GLOBAL_STATE] ?? {};
107
- globalThis[GLOBAL_STATE] = globalState;
108
- let child = globalState.child;
109
- const VIRTUAL_MODULE_ID = "virtual:unplugin-cloudflare-tunnel";
110
- const requestedMode = options.mode;
111
- if (requestedMode && !["quick", "named"].includes(requestedMode)) throw new Error("[unplugin-cloudflare-tunnel] mode must be one of: 'quick', 'named'");
112
- const hasHostname = "hostname" in options && typeof options.hostname === "string";
113
- const isQuickMode = requestedMode ? requestedMode === "quick" : !hasHostname;
114
- if (requestedMode === "named" && !hasHostname) throw new Error("[unplugin-cloudflare-tunnel] hostname is required when mode is set to named");
115
- if (isQuickMode) {
116
- const invalidOptions = [
117
- "apiToken",
118
- "accountId",
119
- "zoneId",
120
- "tunnelName",
121
- "dns",
122
- "ssl",
123
- "cleanup"
124
- ].filter((opt) => opt in options);
125
- if (invalidOptions.length > 0) throw new Error(`[unplugin-cloudflare-tunnel] The following options are only supported in named tunnel mode: ${invalidOptions.join(", ")}. Set mode to 'named' and provide a hostname, or remove these options for quick tunnel mode.`);
126
- }
127
- let providedApiToken;
128
- let hostname;
129
- let tunnelName;
130
- let forcedAccount;
131
- let forcedZone;
132
- let dnsOption;
133
- let sslOption;
134
- let cleanupConfig;
135
- if (isQuickMode) {
136
- tunnelName = "quick-tunnel";
137
- cleanupConfig = {};
138
- } else {
139
- const namedOptions = options;
140
- providedApiToken = namedOptions.apiToken;
141
- hostname = namedOptions.hostname;
142
- forcedAccount = namedOptions.accountId;
143
- forcedZone = namedOptions.zoneId;
144
- tunnelName = namedOptions.tunnelName || "dev-tunnel";
145
- dnsOption = namedOptions.dns;
146
- sslOption = namedOptions.ssl;
147
- cleanupConfig = namedOptions.cleanup || {};
148
- }
149
- const { port: userProvidedPort, logFile, logLevel, protocol = "http2", debug = false } = options;
150
- const effectivePluginLogLevel = logLevel ?? (debug ? "debug" : "info");
151
- const redactForDebug = (value) => {
152
- if (typeof value === "string") {
153
- if (value.startsWith("eyJ") && value.length > 40) return "[REDACTED_TOKEN]";
154
- return value;
155
- }
156
- if (Array.isArray(value)) return value.map((item) => redactForDebug(item));
157
- if (value && typeof value === "object") {
158
- const entries = Object.entries(value).map(([key, nestedValue]) => {
159
- if (/token|authorization|secret|password/i.test(key)) return [key, "[REDACTED]"];
160
- return [key, redactForDebug(nestedValue)];
161
- });
162
- return Object.fromEntries(entries);
163
- }
164
- return value;
165
- };
166
- const formatDebugValue = (value) => {
167
- const redactedValue = redactForDebug(value);
168
- if (typeof redactedValue === "string") return redactedValue;
169
- return NodeUtil.inspect(redactedValue, {
170
- depth: null,
171
- colors: supportsColor(),
172
- compact: false,
173
- breakLength: 120,
174
- sorted: true
175
- });
176
- };
177
- const pluginLog = {
178
- debug: (...args) => {
179
- if (debug || effectivePluginLogLevel === "debug") console.log("[cloudflare-tunnel:debug]", ...args.map((arg) => formatDebugValue(arg)));
180
- },
181
- info: (message) => {
182
- if (shouldLog(effectivePluginLogLevel, "info")) console.log(`[unplugin-cloudflare-tunnel] ${message}`);
183
- },
184
- warn: (message) => {
185
- if (shouldLog(effectivePluginLogLevel, "warn")) console.warn(`[unplugin-cloudflare-tunnel] ${message}`);
186
- },
187
- error: (message) => {
188
- if (shouldLog(effectivePluginLogLevel, "error")) console.error(`[unplugin-cloudflare-tunnel] ${message}`);
189
- }
190
- };
191
- const debugLog = pluginLog.debug;
192
- const makeLocalDisplay = (localTarget) => {
193
- if (!localTarget) return localTarget;
194
- return localTarget.replace("http://[::1]:", "http://localhost:").replace("http://127.0.0.1:", "http://localhost:");
195
- };
196
- const announceConnecting = () => {
197
- if (globalState.__tunnelConnectingAnnounced) return;
198
- globalState.__tunnelConnectingAnnounced = true;
199
- const message = isQuickMode ? "cf tunnel connecting…" : hostname ? `cf tunnel connecting… (${hostname})` : "cf tunnel connecting…";
200
- console.log("");
201
- console.log(colorize(message, ANSI.bold));
202
- };
203
- const announceTunnel = (params) => {
204
- if (!params.url) return;
205
- if (globalState.__lastAnnouncedTunnelKey === params.key) return;
206
- globalState.__lastAnnouncedTunnelKey = params.key;
207
- const cols = process.stdout.columns ?? 80;
208
- const maxWidth = Math.max(10, cols - 2);
209
- const header = `${colorize("[", ANSI.yellow)}unplugin-cloudflare-tunnel${colorize("]", ANSI.yellow)}`;
210
- const urlLine = colorize(params.url, ANSI.blue + ANSI.bold);
211
- const localLine = params.localTarget ? makeLocalDisplay(params.localTarget) : "";
212
- const headerPlainLen = stripAnsi(header).length;
213
- const contentPlainLen = Math.max(stripAnsi(urlLine).length, localLine.length, 10, 5);
214
- const width = Math.min(90, maxWidth, Math.max(44, headerPlainLen, contentPlainLen + 4));
215
- const rule = "─".repeat(width);
216
- const center = (text) => {
217
- const plainLen = stripAnsi(text).length;
218
- const pad = Math.max(0, Math.floor((width - plainLen) / 2));
219
- return `${" ".repeat(pad)}${text}`;
220
- };
221
- if (cols < 70) {
222
- const out = [];
223
- out.push("");
224
- out.push(`${header} ${colorize("Tunnel URL", ANSI.bold)} ${urlLine}`);
225
- if (localLine) out.push(`${header} ${colorize("Local", ANSI.dim + ANSI.bold)} ${localLine}`);
226
- out.push("");
227
- console.log(out.join("\n"));
228
- return;
229
- }
230
- const out = [];
231
- out.push("");
232
- out.push(center(header));
233
- out.push(rule);
234
- out.push(center(colorize("Tunnel URL", ANSI.bold)));
235
- out.push(center(urlLine));
236
- if (localLine) {
237
- out.push("");
238
- out.push(center(colorize("Local", ANSI.dim + ANSI.bold)));
239
- out.push(center(localLine));
240
- }
241
- out.push(rule);
242
- out.push("");
243
- console.log(out.join("\n"));
244
- };
245
- if (!isQuickMode && (!hostname || typeof hostname !== "string")) throw new Error("[unplugin-cloudflare-tunnel] hostname is required and must be a valid string in named tunnel mode");
246
- let tunnelUrl = hostname ? `https://${hostname}` : "";
247
- if (tunnelName && !/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(tunnelName)) throw new Error("[unplugin-cloudflare-tunnel] tunnelName must contain only letters, numbers, and hyphens. It cannot start or end with a hyphen.");
248
- if (userProvidedPort && (typeof userProvidedPort !== "number" || userProvidedPort < 1 || userProvidedPort > 65535)) throw new Error("[unplugin-cloudflare-tunnel] port must be a valid number between 1 and 65535");
249
- if (logLevel && ![
250
- "debug",
251
- "info",
252
- "warn",
253
- "error",
254
- "fatal"
255
- ].includes(logLevel)) throw new Error("[unplugin-cloudflare-tunnel] logLevel must be one of: debug, info, warn, error, fatal");
256
- const effectiveLogLevel = logLevel ?? (debug ? "info" : "warn");
257
- const cloudflaredProcessLogLevel = effectiveLogLevel === "debug" ? "debug" : "info";
258
- debugLog("Effective cloudflared log level filter:", effectiveLogLevel);
259
- debugLog("Effective cloudflared process log level:", cloudflaredProcessLogLevel);
260
- debugLog("Effective cloudflared protocol:", protocol);
261
- if (dnsOption) {
262
- if (!dnsOption.startsWith("*.") && dnsOption !== hostname) throw new Error("[unplugin-cloudflare-tunnel] dns option must either be a wildcard (e.g., '*.example.com') or exactly match the hostname");
263
- }
264
- if (sslOption) {
265
- if (!sslOption.startsWith("*.") && sslOption !== hostname) throw new Error("[unplugin-cloudflare-tunnel] ssl option must either be a wildcard (e.g., '*.example.com') or exactly match the hostname");
266
- }
267
- if (!["http2", "quic"].includes(protocol)) throw new Error("[unplugin-cloudflare-tunnel] protocol must be one of: 'http2', 'quic'");
268
- const trackSslCertificate = (certificateId, hosts, tunnelName, timestamp = (/* @__PURE__ */ new Date()).toISOString()) => {
269
- const trackingKey = `ssl-cert-${certificateId}`;
270
- globalState[trackingKey] = {
271
- id: certificateId,
272
- hosts,
273
- tunnelName,
274
- timestamp,
275
- pluginVersion: "1.0.0"
276
- };
277
- debugLog(`Tracking SSL certificate: ${certificateId} for hosts: ${hosts.join(", ")}`);
278
- };
279
- const findMismatchedSslCertificates = async (apiToken, zoneId, currentTunnelName, currentHostname) => {
280
- try {
281
- const certPacks = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, any());
282
- const currentTunnelCerts = (Array.isArray(certPacks) ? certPacks : certPacks.result || []).filter((cert) => {
283
- return (cert.hostnames || cert.hosts || []).some((host) => host.startsWith(`cf-tunnel-plugin-${currentTunnelName}--`));
284
- });
285
- debugLog(`Found ${currentTunnelCerts.length} SSL certificates for current tunnel: ${currentTunnelName}`);
286
- const mismatchedCerts = currentTunnelCerts.filter((cert) => {
287
- return !(cert.hostnames || cert.hosts || []).some((host) => {
288
- if (host.startsWith("cf-tunnel-plugin-")) return false;
289
- return host === currentHostname || host.startsWith("*.") && currentHostname.endsWith(host.slice(1));
290
- });
291
- });
292
- debugLog(`Found ${mismatchedCerts.length} mismatched SSL certificates`, mismatchedCerts.map((c) => ({
293
- id: c.id,
294
- hosts: c.hostnames || c.hosts,
295
- currentHostname
296
- })));
297
- return mismatchedCerts;
298
- } catch (error) {
299
- console.error(`[unplugin-cloudflare-tunnel] ❌ SSL certificate listing failed: ${error.message}`);
300
- return [];
301
- }
302
- };
303
- const cleanupMismatchedDnsRecords = async (apiToken, zoneId, dnsComment, currentHostname, tunnelId) => {
304
- try {
305
- const pluginDnsRecords = await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?comment=${dnsComment}&match=all`, void 0, array(DNSRecordSchema));
306
- debugLog(`Found ${pluginDnsRecords.length} DNS records for current tunnel: ${dnsComment}`);
307
- const expectedCnameContent = `${tunnelId}.cfargotunnel.com`;
308
- const mismatchedRecords = pluginDnsRecords.filter((record) => {
309
- if (record.name === currentHostname && record.content === expectedCnameContent) return false;
310
- if (dnsOption && record.name === dnsOption && record.content === expectedCnameContent) return false;
311
- return true;
312
- });
313
- debugLog(`Found ${mismatchedRecords.length} mismatched DNS records`, mismatchedRecords.map((r) => ({
314
- name: r.name,
315
- content: r.content,
316
- expected: expectedCnameContent,
317
- comment: r.comment
318
- })));
319
- const deletedRecords = [];
320
- if (mismatchedRecords.length > 0) {
321
- console.log(`[unplugin-cloudflare-tunnel] 🧹 Cleaning up ${mismatchedRecords.length} mismatched DNS records from tunnel '${dnsComment}'...`);
322
- for (const record of mismatchedRecords) try {
323
- await cf(apiToken, "DELETE", `/zones/${zoneId}/dns_records/${record.id}`);
324
- deletedRecords.push(record);
325
- console.log(`[unplugin-cloudflare-tunnel] ✅ Deleted mismatched DNS record: ${record.name} → ${record.content}`);
326
- } catch (error) {
327
- console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to delete DNS record ${record.name}: ${error.message}`);
328
- }
329
- }
330
- return {
331
- found: mismatchedRecords,
332
- deleted: deletedRecords
333
- };
334
- } catch (error) {
335
- console.error(`[unplugin-cloudflare-tunnel] ❌ DNS cleanup failed: ${error.message}`);
336
- return {
337
- found: [],
338
- deleted: []
339
- };
340
- }
341
- };
342
- const cf = async (apiToken, method, url, body, resultSchema) => {
343
- try {
344
- debugLog("→ CF API", method, url, body ? { body } : "");
345
- const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
346
- method,
347
- headers: {
348
- Authorization: `Bearer ${apiToken}`,
349
- "Content-Type": "application/json",
350
- "User-Agent": "unplugin-cloudflare-tunnel/1.0.0"
351
- },
352
- ...body ? { body: JSON.stringify(body) } : {}
353
- });
354
- if (!response.ok) {
355
- const errorText = await response.text().catch(() => "Unknown error");
356
- throw new Error(`[unplugin-cloudflare-tunnel] API request failed: ${response.status} ${response.statusText}. Response: ${errorText}`);
357
- }
358
- const rawData = await response.json();
359
- debugLog("← CF API response", rawData);
360
- const apiResponse = CloudflareApiResponseSchema.parse(rawData);
361
- if (!apiResponse.success) {
362
- const errorMsg = apiResponse.errors?.map((e) => e.message || `Error ${e.code}`).join(", ") || "Unknown API error";
363
- throw new Error(`[unplugin-cloudflare-tunnel] Cloudflare API error: ${errorMsg}`);
364
- }
365
- if (resultSchema) {
366
- const parsed = resultSchema.parse(apiResponse.result);
367
- debugLog("← Parsed result", parsed);
368
- return parsed;
369
- }
370
- debugLog("← Result (untyped)", apiResponse.result);
371
- return apiResponse.result;
372
- } catch (error) {
373
- if (error instanceof Error) {
374
- if (error.message.includes("[unplugin-cloudflare-tunnel]")) throw error;
375
- throw new Error(`[unplugin-cloudflare-tunnel] API request failed: ${error.message}`);
376
- }
377
- throw new Error("[unplugin-cloudflare-tunnel] Unknown API error occurred");
378
- }
379
- };
380
- const retryWithBackoff = async (fn, maxRetries = 5, initialDelayMs = 1e3) => {
381
- let attempt = 0;
382
- while (true) try {
383
- return await fn();
384
- } catch (error) {
385
- attempt += 1;
386
- const message = error instanceof Error ? error.message : String(error);
387
- if (attempt > maxRetries) {
388
- console.error(`[unplugin-cloudflare-tunnel] ❌ Edge certificate request failed after ${maxRetries} retries: ${message}`);
389
- throw error;
390
- }
391
- const delay = initialDelayMs * 2 ** (attempt - 1);
392
- console.error(`[unplugin-cloudflare-tunnel] ⚠️ Edge certificate request failed (attempt ${attempt}/${maxRetries}): ${message}`);
393
- console.error(`[unplugin-cloudflare-tunnel] ⏳ Retrying in ${delay}ms...`);
394
- await new Promise((resolve) => setTimeout(resolve, delay));
395
- }
396
- };
397
- const spawnQuickTunnel = async (localTarget, protocol) => {
398
- const cloudflaredArgs = ["tunnel"];
399
- cloudflaredArgs.push("--loglevel", "info");
400
- if (logFile) cloudflaredArgs.push("--logfile", logFile);
401
- cloudflaredArgs.push("--protocol", protocol);
402
- cloudflaredArgs.push("--url", localTarget);
403
- debugLog("Spawning quick tunnel:", bin, cloudflaredArgs);
404
- const child = NodeChildProcess.spawn(bin, cloudflaredArgs, {
405
- stdio: [
406
- "ignore",
407
- "pipe",
408
- "pipe"
409
- ],
410
- detached: false,
411
- windowsHide: true,
412
- shell: process.platform === "win32"
413
- });
414
- debugLog(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child.pid}`);
415
- return new Promise((resolve, reject) => {
416
- let urlFound = false;
417
- let settled = false;
418
- const rejectOnce = (error) => {
419
- if (settled) return;
420
- settled = true;
421
- reject(error);
422
- };
423
- const resolveOnce = (result) => {
424
- if (settled) return;
425
- settled = true;
426
- resolve(result);
427
- };
428
- const timeout = setTimeout(() => {
429
- if (!urlFound) {
430
- try {
431
- child.kill("SIGTERM");
432
- } catch {}
433
- rejectOnce(/* @__PURE__ */ new Error("Quick tunnel URL not found in output within 30 seconds"));
434
- }
435
- }, 3e4);
436
- child.stdout?.on("data", (data) => {
437
- const output = data.toString();
438
- if (!globalState.shuttingDown || debug) {
439
- if (effectiveLogLevel === "debug" || effectiveLogLevel === "info") console.log(`[cloudflared stdout] ${output.trim()}`);
440
- else for (const line of output.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.log(`[cloudflared stdout] ${line.trim()}`);
441
- }
442
- });
443
- child.stderr?.on("data", (data) => {
444
- const error = data.toString().trim();
445
- const urlMatch = error.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
446
- if (urlMatch && !urlFound) {
447
- urlFound = true;
448
- clearTimeout(timeout);
449
- resolveOnce({
450
- child,
451
- url: urlMatch[0]
452
- });
453
- }
454
- if (error.includes("Failed to parse ICMP reply") || error.includes("unknow ip version 0")) {
455
- if (logLevel === "debug") console.log(`[cloudflared debug] ${error}`);
456
- return;
457
- }
458
- if (!globalState.shuttingDown || debug) {
459
- if (effectiveLogLevel === "debug" || effectiveLogLevel === "info") console.error(`[cloudflared stderr] ${error}`);
460
- else for (const line of error.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.error(`[cloudflared stderr] ${line.trim()}`);
461
- }
462
- });
463
- child.on("error", (error) => {
464
- clearTimeout(timeout);
465
- rejectOnce(/* @__PURE__ */ new Error(`Failed to start quick tunnel process: ${error.message}`));
466
- });
467
- child.on("exit", (code, signal) => {
468
- clearTimeout(timeout);
469
- if (!urlFound) rejectOnce(/* @__PURE__ */ new Error(`Quick tunnel process exited before URL was found (code: ${code}, signal: ${signal})`));
470
- });
471
- });
472
- };
473
- const killCloudflared = (signal = "SIGTERM") => {
474
- if (!child || child.killed) return;
475
- globalState.shuttingDown = true;
476
- globalState.tunnelUrl = void 0;
477
- try {
478
- debugLog(`[unplugin-cloudflare-tunnel] Terminating cloudflared process (PID: ${child.pid}) with ${signal}...`);
479
- if (!child.kill(signal) && process.platform === "win32") NodeChildProcess.exec(`taskkill /pid ${child.pid} /T /F`, () => {});
480
- if (signal === "SIGTERM") setTimeout(() => {
481
- if (child && !child.killed) {
482
- debugLog("[unplugin-cloudflare-tunnel] Force killing cloudflared process...");
483
- if (process.platform === "win32") NodeChildProcess.exec(`taskkill /pid ${child.pid} /T /F`, () => {});
484
- else child.kill("SIGKILL");
485
- }
486
- }, 2e3);
487
- } catch (error) {
488
- debugLog(`[unplugin-cloudflare-tunnel] Note: Error killing cloudflared: ${error}`);
489
- }
490
- };
491
- let exitHandlersRegistered = globalState.exitHandlersRegistered ?? false;
492
- const registerExitHandler = () => {
493
- if (exitHandlersRegistered) return;
494
- exitHandlersRegistered = true;
495
- globalState.exitHandlersRegistered = true;
496
- const cleanup = () => killCloudflared("SIGTERM");
497
- process.once("exit", cleanup);
498
- process.once("beforeExit", cleanup);
499
- [
500
- "SIGINT",
501
- "SIGTERM",
502
- "SIGQUIT",
503
- "SIGHUP"
504
- ].forEach((signal) => {
505
- process.once(signal, () => {
506
- killCloudflared(signal);
507
- try {
508
- process.kill(process.pid, signal);
509
- } catch {
510
- process.exit(0);
511
- }
512
- });
513
- });
514
- process.once("uncaughtException", (error) => {
515
- console.error("[unplugin-cloudflare-tunnel] Uncaught exception, cleaning up cloudflared...", error);
516
- killCloudflared("SIGTERM");
517
- });
518
- process.once("unhandledRejection", (reason) => {
519
- console.error("[unplugin-cloudflare-tunnel] Unhandled rejection, cleaning up cloudflared...", reason);
520
- killCloudflared("SIGTERM");
521
- });
522
- };
523
- const configureServer = async (server) => {
524
- const generateDnsComment = () => {
525
- return `unplugin-cloudflare-tunnel:${tunnelName}`;
526
- };
527
- const registerListeningHandler = (handler) => {
528
- const httpServer = server.httpServer;
529
- if (!httpServer) return;
530
- const invokeHandler = () => {
531
- try {
532
- const maybePromise = handler();
533
- if (maybePromise && typeof maybePromise.then === "function") maybePromise.catch((error) => {
534
- console.error(`[unplugin-cloudflare-tunnel] ❌ Dev server listening hook failed: ${error.message}`);
535
- });
536
- } catch (error) {
537
- console.error(`[unplugin-cloudflare-tunnel] ❌ Dev server listening hook failed: ${error.message}`);
538
- }
539
- };
540
- httpServer.on("listening", invokeHandler);
541
- if (httpServer.listening) invokeHandler();
542
- };
543
- try {
544
- const { host: serverHost, port: detectedPort } = normalizeAddress(server.httpServer?.address());
545
- const configPortValue = server.config?.server?.port;
546
- const resolvedConfigPort = typeof configPortValue === "string" ? Number.parseInt(configPortValue, 10) : configPortValue;
547
- const port = userProvidedPort || detectedPort || (typeof resolvedConfigPort === "number" && !Number.isNaN(resolvedConfigPort) ? resolvedConfigPort : void 0) || 5173;
548
- const newConfigHash = JSON.stringify({
549
- isQuickMode,
550
- hostname,
551
- port,
552
- tunnelName,
553
- dnsOption,
554
- sslOption
555
- });
556
- if (globalState.child && !globalState.child.killed && globalState.configHash === newConfigHash) {
557
- tunnelUrl = await globalState.tunnelUrl ?? "";
558
- debugLog("[unplugin-cloudflare-tunnel] Config unchanged – re-using existing tunnel");
559
- globalState.shuttingDown = false;
560
- registerExitHandler();
561
- return;
562
- }
563
- if (globalState.child && !globalState.child.killed) {
564
- debugLog("[unplugin-cloudflare-tunnel] Config changed – terminating previous tunnel...");
565
- try {
566
- globalState.child.kill("SIGTERM");
567
- } catch (_) {}
568
- }
569
- delete globalState.child;
570
- delete globalState.configHash;
571
- globalState.shuttingDown = false;
572
- if (isQuickMode) {
573
- debugLog("[unplugin-cloudflare-tunnel] Starting quick tunnel mode...");
574
- debugLog("Quick tunnel mode - no API token or hostname required");
575
- await ensureCloudflaredBinary(bin);
576
- const localTarget = getLocalTarget(serverHost, port);
577
- debugLog("← Quick tunnel connecting to local target", localTarget);
578
- try {
579
- const { child: quickChild, url } = await spawnQuickTunnel(localTarget, protocol);
580
- tunnelUrl = url;
581
- child = quickChild;
582
- globalState.child = child;
583
- globalState.configHash = newConfigHash;
584
- globalState.tunnelUrl = Promise.resolve(url);
585
- registerExitHandler();
586
- registerListeningHandler(() => {
587
- const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
588
- const actualLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
589
- announceTunnel({
590
- key: `quick:${url}:${actualPort ?? port}`,
591
- url,
592
- localTarget: actualLocalTarget
593
- });
594
- });
595
- registerListeningHandler(async () => {
596
- try {
597
- const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
598
- if (actualPort !== port) {
599
- pluginLog.warn(`Port conflict detected - server is using port ${actualPort} instead of ${port}`);
600
- pluginLog.info("Restarting quick tunnel for the new port...");
601
- killCloudflared("SIGTERM");
602
- await new Promise((resolve) => setTimeout(resolve, 1e3));
603
- const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
604
- const { child: newChild, url: newUrl } = await spawnQuickTunnel(newLocalTarget, protocol);
605
- tunnelUrl = newUrl;
606
- child = newChild;
607
- globalState.child = child;
608
- globalState.tunnelUrl = Promise.resolve(newUrl);
609
- announceTunnel({
610
- key: `quick:${newUrl}:${actualPort ?? port}`,
611
- url: newUrl,
612
- localTarget: newLocalTarget
613
- });
614
- globalState.configHash = JSON.stringify({
615
- isQuickMode,
616
- hostname,
617
- port: actualPort,
618
- tunnelName,
619
- dnsOption,
620
- sslOption
621
- });
622
- }
623
- } catch (error) {
624
- console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to update quick tunnel for port change: ${error.message}`);
625
- }
626
- });
627
- server.httpServer?.once("close", () => {
628
- killCloudflared("SIGTERM");
629
- });
630
- return;
631
- } catch (error) {
632
- console.error(`[unplugin-cloudflare-tunnel] ❌ Quick tunnel setup failed: ${error.message}`);
633
- throw error;
634
- }
635
- }
636
- debugLog("[unplugin-cloudflare-tunnel] Starting named tunnel mode...");
637
- const apiToken = providedApiToken || process.env.CLOUDFLARE_API_TOKEN;
638
- if (!apiToken) throw new Error("[unplugin-cloudflare-tunnel] API token is required. Provide it via 'apiToken' option or set the CLOUDFLARE_API_TOKEN environment variable. Get your token at: https://dash.cloudflare.com/profile/api-tokens");
639
- debugLog(`[unplugin-cloudflare-tunnel] Using port ${port}${userProvidedPort === port ? " (user-provided)" : " (from bundler config)"}`);
640
- await ensureCloudflaredBinary(bin);
641
- const apexDomain = hostname.split(".").slice(-2).join(".");
642
- const parentDomain = hostname.split(".").slice(1).join(".");
643
- debugLog("← Apex domain", apexDomain);
644
- debugLog("← Parent domain", parentDomain);
645
- let resolvedZone;
646
- let zoneId = forcedZone;
647
- if (!zoneId) {
648
- let zones = [];
649
- try {
650
- zones = await cf(apiToken, "GET", `/zones?name=${parentDomain}`, void 0, array(ZoneSchema));
651
- } catch (error) {
652
- debugLog("← Error fetching zone for parent domain", error);
653
- }
654
- if (zones.length === 0) zones = await cf(apiToken, "GET", `/zones?name=${apexDomain}`, void 0, array(ZoneSchema));
655
- resolvedZone = zones[0];
656
- zoneId = resolvedZone?.id;
657
- }
658
- let accountId = forcedAccount || resolvedZone?.account?.id;
659
- if (!accountId) accountId = (await cf(apiToken, "GET", "/accounts", void 0, array(AccountSchema)))[0]?.id;
660
- if (!accountId) throw new Error("Unable to determine Cloudflare account ID");
661
- if (!zoneId) throw new Error(`Zone ${apexDomain} not found in account ${accountId}`);
662
- const { autoCleanup = true } = cleanupConfig;
663
- let tunnel = (await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel?name=${tunnelName}`, void 0, array(TunnelSchema)))[0];
664
- if (!tunnel) {
665
- pluginLog.info(`Creating tunnel '${tunnelName}'...`);
666
- tunnel = await cf(apiToken, "POST", `/accounts/${accountId}/cfd_tunnel`, {
667
- name: tunnelName,
668
- config_src: "cloudflare"
669
- }, TunnelSchema);
670
- }
671
- const tunnelId = tunnel.id;
672
- if (autoCleanup) {
673
- debugLog(`[unplugin-cloudflare-tunnel] Running resource cleanup for tunnel '${tunnelName}'...`);
674
- const dnsCleanup = await cleanupMismatchedDnsRecords(apiToken, zoneId, generateDnsComment(), hostname, tunnelId);
675
- if (dnsCleanup.found.length > 0) pluginLog.warn(`DNS cleanup: ${dnsCleanup.found.length} mismatched, ${dnsCleanup.deleted.length} deleted`);
676
- const mismatchedSslCerts = await findMismatchedSslCertificates(apiToken, zoneId, tunnelName, hostname);
677
- if (mismatchedSslCerts.length > 0) {
678
- for (const cert of mismatchedSslCerts) await cf(apiToken, "DELETE", `/zones/${zoneId}/ssl/certificate_packs/${cert.id}`);
679
- pluginLog.warn(`SSL cleanup: ${mismatchedSslCerts.length} deleted`);
680
- }
681
- } else debugLog("← Cleanup skipped", cleanupConfig);
682
- const localTarget = getLocalTarget(serverHost, port);
683
- debugLog("← Connecting to local target", localTarget);
684
- await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
685
- hostname,
686
- service: localTarget
687
- }, { service: "http_status:404" }] } });
688
- const generateSslTagHostname = () => {
689
- return `cf-tunnel-plugin-${tunnelName}--${parentDomain}`;
690
- };
691
- if (dnsOption) {
692
- const ensureDnsRecord = async (type, content) => {
693
- if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(dnsOption)}`, void 0, array(DNSRecordSchema))).length === 0) {
694
- console.log(`[unplugin-cloudflare-tunnel] Creating ${type} record for ${dnsOption}...`);
695
- await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
696
- type,
697
- name: dnsOption,
698
- content,
699
- proxied: true,
700
- comment: generateDnsComment()
701
- }, DNSRecordSchema);
702
- }
703
- };
704
- await ensureDnsRecord("CNAME", `${tunnelId}.cfargotunnel.com`);
705
- } else {
706
- const wildcardDns = `*.${parentDomain}`;
707
- if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${wildcardDns}`, void 0, array(DNSRecordSchema))).length === 0) {
708
- const existingRecord = (await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${hostname}`, void 0, array(DNSRecordSchema)))[0];
709
- const expectedContent = `${tunnelId}.cfargotunnel.com`;
710
- if (!existingRecord) {
711
- console.log(`[unplugin-cloudflare-tunnel] Creating DNS record for ${hostname}...`);
712
- await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
713
- type: "CNAME",
714
- name: hostname,
715
- content: expectedContent,
716
- proxied: true,
717
- comment: generateDnsComment()
718
- }, DNSRecordSchema);
719
- } else if (existingRecord.content !== expectedContent) {
720
- debugLog(`← DNS record for ${hostname} points to different tunnel, updating...`);
721
- pluginLog.info(`Updating DNS record for ${hostname} to point to tunnel '${tunnelName}'...`);
722
- await cf(apiToken, "PUT", `/zones/${zoneId}/dns_records/${existingRecord.id}`, {
723
- type: "CNAME",
724
- name: hostname,
725
- content: expectedContent,
726
- proxied: true,
727
- comment: generateDnsComment()
728
- }, DNSRecordSchema);
729
- }
730
- }
731
- }
732
- const token = await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`, void 0, string());
733
- try {
734
- const certListRaw = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, any());
735
- const certPacks = Array.isArray(certListRaw) ? certListRaw : certListRaw.result || [];
736
- const certContainingHost = (host) => certPacks.filter((c) => (c.hostnames || c.hosts || []).includes(host))?.[0];
737
- if (sslOption) {
738
- const isWildcard = sslOption.startsWith("*.");
739
- const certNeededHost = sslOption;
740
- const matchingCert = certContainingHost(certNeededHost);
741
- if (!matchingCert) {
742
- console.log(`[unplugin-cloudflare-tunnel] Requesting ${isWildcard ? "wildcard " : ""}certificate for ${certNeededHost}...`);
743
- const tagHostname = generateSslTagHostname();
744
- const certificateHosts = [certNeededHost, tagHostname];
745
- debugLog(`Adding tag hostname to certificate: ${tagHostname}`);
746
- const newCert = await retryWithBackoff(() => cf(apiToken, "POST", `/zones/${zoneId}/ssl/certificate_packs/order`, {
747
- hosts: certificateHosts,
748
- certificate_authority: "lets_encrypt",
749
- type: "advanced",
750
- validation_method: isWildcard ? "txt" : "http",
751
- validity_days: 90,
752
- cloudflare_branding: false
753
- }));
754
- if (newCert?.id) trackSslCertificate(newCert.id, certificateHosts, tunnelName);
755
- } else debugLog("← Edge certificate already exists", matchingCert);
756
- } else {
757
- const wildcardDomain = `*.${parentDomain}`;
758
- const wildcardExists = certContainingHost(wildcardDomain);
759
- if (!wildcardExists) {
760
- const totalTls = await cf(apiToken, "GET", `/zones/${zoneId}/acm/total_tls`, void 0, object({ status: string() }));
761
- debugLog("← Total TLS", totalTls);
762
- const existingHostnameCert = certContainingHost(hostname);
763
- if (totalTls.status !== "on" && !existingHostnameCert) {
764
- pluginLog.info(`Requesting edge certificate for ${hostname}...`);
765
- const tagHostname = generateSslTagHostname();
766
- const certificateHosts = [hostname, tagHostname];
767
- debugLog(`Adding tag hostname to certificate: ${tagHostname}`);
768
- const newCert = await retryWithBackoff(() => cf(apiToken, "POST", `/zones/${zoneId}/ssl/certificate_packs/order`, {
769
- hosts: certificateHosts,
770
- certificate_authority: "lets_encrypt",
771
- type: "advanced",
772
- validation_method: "txt",
773
- validity_days: 90,
774
- cloudflare_branding: false
775
- }));
776
- if (newCert?.id) trackSslCertificate(newCert.id, certificateHosts, tunnelName);
777
- } else debugLog("← Edge certificate already exists", existingHostnameCert);
778
- } else debugLog("← Edge certificate (wildcard) already exists", wildcardExists, wildcardDomain);
779
- }
780
- } catch (sslError) {
781
- console.error(`[unplugin-cloudflare-tunnel] ⚠️ SSL management error: ${sslError.message}`);
782
- throw sslError;
783
- }
784
- let tunnelReady = false;
785
- let localTargetForAnnouncement = localTarget;
786
- let activeTunnelProtocol;
787
- const announceNamedTunnelIfReady = () => {
788
- if (!tunnelReady) return;
789
- announceTunnel({
790
- key: `named:${hostname}:${localTargetForAnnouncement}`,
791
- url: `https://${hostname}`,
792
- localTarget: localTargetForAnnouncement
793
- });
794
- };
795
- const logCloudflaredLines = (kind, text) => {
796
- if (globalState.shuttingDown && !debug) return;
797
- const isVerbose = effectiveLogLevel === "debug" || effectiveLogLevel === "info";
798
- const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
799
- if (isVerbose) {
800
- for (const line of lines) {
801
- const prefix = kind === "stdout" ? "[cloudflared stdout]" : "[cloudflared stderr]";
802
- if (kind === "stdout") console.log(`${prefix} ${line}`);
803
- else console.error(`${prefix} ${line}`);
804
- }
805
- return;
806
- }
807
- for (const line of lines) {
808
- if (INFO_LOG_REGEX.test(line)) continue;
809
- const prefix = kind === "stdout" ? "[cloudflared stdout]" : "[cloudflared stderr]";
810
- if (kind === "stdout") console.log(`${prefix} ${line}`);
811
- else console.error(`${prefix} ${line}`);
812
- }
813
- };
814
- const spawnNamedTunnelProcess = (protocol) => {
815
- const cloudflaredArgs = ["tunnel"];
816
- cloudflaredArgs.push("--loglevel", cloudflaredProcessLogLevel);
817
- if (logFile) cloudflaredArgs.push("--logfile", logFile);
818
- cloudflaredArgs.push("--protocol", protocol);
819
- debugLog("Spawning cloudflared", bin, cloudflaredArgs);
820
- const spawnedChild = NodeChildProcess.spawn(bin, [
821
- ...cloudflaredArgs,
822
- "run",
823
- "--token",
824
- token
825
- ], {
826
- stdio: [
827
- "ignore",
828
- "pipe",
829
- "pipe"
830
- ],
831
- detached: false,
832
- windowsHide: true,
833
- shell: process.platform === "win32"
834
- });
835
- child = spawnedChild;
836
- globalState.child = spawnedChild;
837
- globalState.configHash = newConfigHash;
838
- debugLog(`[unplugin-cloudflare-tunnel] Process spawned with PID: ${spawnedChild.pid}`);
839
- const handleCloudflaredOutput = (kind, text) => {
840
- if (text.includes("Failed to parse ICMP reply") || text.includes("unknow ip version 0")) {
841
- if (logLevel === "debug") console.log(`[cloudflared debug] ${text.trim()}`);
842
- return;
843
- }
844
- logCloudflaredLines(kind, text);
845
- if (/registered tunnel connection|connection.*registered/i.test(text)) {
846
- activeTunnelProtocol = protocol;
847
- if (!tunnelReady) {
848
- tunnelReady = true;
849
- pluginLog.info(`Tunnel connected for https://${hostname} via ${protocol.toUpperCase()}`);
850
- }
851
- announceNamedTunnelIfReady();
852
- }
853
- };
854
- spawnedChild.stdout?.on("data", (data) => {
855
- handleCloudflaredOutput("stdout", data.toString());
856
- });
857
- spawnedChild.stderr?.on("data", (data) => {
858
- handleCloudflaredOutput("stderr", data.toString());
859
- });
860
- spawnedChild.on("error", (error) => {
861
- console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to start tunnel process: ${error.message}`);
862
- if (error.message.includes("ENOENT")) console.error(`[unplugin-cloudflare-tunnel] Hint: cloudflared binary may not be installed correctly`);
863
- });
864
- spawnedChild.on("exit", (code, signal) => {
865
- if (globalState.child !== spawnedChild) return;
866
- if (code !== 0 && code !== null) {
867
- console.error(`[unplugin-cloudflare-tunnel] ❌ Tunnel process exited with code ${code}`);
868
- if (signal) console.error(`[unplugin-cloudflare-tunnel] Process terminated by signal: ${signal}`);
869
- } else if (code === 0) console.log(`[unplugin-cloudflare-tunnel] ✅ Tunnel process exited cleanly`);
870
- });
871
- };
872
- spawnNamedTunnelProcess(protocol);
873
- registerExitHandler();
874
- registerListeningHandler(() => {
875
- const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
876
- localTargetForAnnouncement = getLocalTarget(actualServerHost, actualPort ?? port);
877
- announceNamedTunnelIfReady();
878
- });
879
- server.httpServer?.once("close", () => {
880
- killCloudflared("SIGTERM");
881
- });
882
- registerListeningHandler(async () => {
883
- try {
884
- const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
885
- if (actualPort !== port) {
886
- pluginLog.warn(`Port conflict detected - server is using port ${actualPort} instead of ${port}`);
887
- pluginLog.info("Updating tunnel configuration...");
888
- const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
889
- localTargetForAnnouncement = newLocalTarget;
890
- debugLog("← Updating local target to", newLocalTarget);
891
- await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
892
- hostname,
893
- service: newLocalTarget
894
- }, { service: "http_status:404" }] } });
895
- pluginLog.info(`Tunnel configuration updated to use port ${actualPort}`);
896
- globalState.configHash = JSON.stringify({
897
- hostname,
898
- port: actualPort,
899
- tunnelName,
900
- dnsOption,
901
- sslOption
902
- });
903
- if (tunnelReady && activeTunnelProtocol) pluginLog.info(`Tunnel remains connected via ${activeTunnelProtocol.toUpperCase()} after port update`);
904
- announceNamedTunnelIfReady();
905
- }
906
- } catch (error) {
907
- console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to update tunnel for port change: ${error.message}`);
908
- }
909
- });
910
- } catch (error) {
911
- if (error instanceof Error) {
912
- console.error(`[unplugin-cloudflare-tunnel] ❌ Setup failed: ${error.message}`);
913
- if (error.message.includes("API token")) {
914
- console.error(`[unplugin-cloudflare-tunnel] 💡 Check your API token at: https://dash.cloudflare.com/profile/api-tokens`);
915
- console.error(`[unplugin-cloudflare-tunnel] 💡 Required permissions: Zone:Zone:Read, Zone:DNS:Edit, Account:Cloudflare Tunnel:Edit`);
916
- } else if (error.message.includes("Zone") && error.message.includes("not found")) console.error(`[unplugin-cloudflare-tunnel] 💡 Make sure '${hostname}' domain is added to your Cloudflare account`);
917
- else if (error.message.includes("cloudflared")) console.error(`[unplugin-cloudflare-tunnel] 💡 Try deleting node_modules and reinstalling to get a fresh cloudflared binary`);
918
- }
919
- throw error;
920
- }
921
- };
922
- const ensureWebpackAllowedHosts = (devServerOptions, bundler) => {
923
- if (!devServerOptions) return;
924
- const hostToAllow = isQuickMode ? ".trycloudflare.com" : hostname;
925
- if (!hostToAllow) return;
926
- const label = bundler === "webpack" ? "Webpack" : "Rspack";
927
- const normalizeArray = (values) => {
928
- const unique = Array.from(new Set(values.filter(Boolean)));
929
- devServerOptions.allowedHosts = unique;
930
- return unique;
931
- };
932
- let modified = false;
933
- const current = devServerOptions.allowedHosts;
934
- if (current === "all") return;
935
- if (typeof current === "undefined" || current === "auto") {
936
- normalizeArray(["localhost", hostToAllow]);
937
- modified = true;
938
- } else if (typeof current === "string") {
939
- if (current !== hostToAllow) {
940
- normalizeArray([current, hostToAllow]);
941
- modified = true;
942
- }
943
- } else if (Array.isArray(current)) {
944
- if (!current.includes(hostToAllow)) {
945
- current.push(hostToAllow);
946
- modified = true;
947
- }
948
- }
949
- if (modified) debugLog(`[unplugin-cloudflare-tunnel] Configured ${label} devServer.allowedHosts to include ${hostToAllow}`);
950
- };
951
- const ensureViteAllowedHosts = (serverConfig) => {
952
- const hostToAllow = isQuickMode ? ".trycloudflare.com" : hostname;
953
- if (!hostToAllow) return;
954
- const current = serverConfig.allowedHosts;
955
- if (current === true) return;
956
- if (typeof current === "undefined") serverConfig.allowedHosts = [hostToAllow];
957
- else if (typeof current === "string") {
958
- if (current !== hostToAllow) serverConfig.allowedHosts = [current, hostToAllow];
959
- } else if (Array.isArray(current)) {
960
- if (!current.includes(hostToAllow)) current.push(hostToAllow);
961
- }
962
- };
963
- const setupWebpackLikeDevServerIntegration = (compiler, bundler) => {
964
- if ((compiler?.options?.mode ?? process.env.NODE_ENV) === "production") return;
965
- const optionsContainer = compiler.options;
966
- if (!optionsContainer.devServer) optionsContainer.devServer = {};
967
- const devServerOptions = optionsContainer.devServer;
968
- ensureWebpackAllowedHosts(devServerOptions, bundler);
969
- let lastHttpServer;
970
- let missingServerWarned = false;
971
- const runConfiguration = (devServerInstance) => {
972
- if (!devServerInstance) {
973
- if (!missingServerWarned) {
974
- console.warn(`[unplugin-cloudflare-tunnel] ${bundler} dev server instance unavailable; skipping tunnel setup`);
975
- missingServerWarned = true;
976
- }
977
- return;
978
- }
979
- const httpServer = [
980
- devServerInstance.server,
981
- devServerInstance.httpServer,
982
- devServerInstance.listeningApp,
983
- devServerInstance.server?.server
984
- ].find((candidate) => candidate);
985
- if (!httpServer) {
986
- if (!missingServerWarned) {
987
- console.warn(`[unplugin-cloudflare-tunnel] Unable to locate HTTP server from ${bundler} dev server; tunnel will not start`);
988
- missingServerWarned = true;
989
- }
990
- return;
991
- }
992
- if (lastHttpServer === httpServer) return;
993
- lastHttpServer = httpServer;
994
- httpServer.once("close", () => {
995
- if (lastHttpServer === httpServer) lastHttpServer = void 0;
996
- });
997
- const configuredPromise = configureServer({
998
- httpServer,
999
- config: { server: { port: devServerInstance.options?.port ?? devServerOptions?.port } }
1000
- });
1001
- globalState.tunnelUrl = configuredPromise.then(() => tunnelUrl).catch(() => "");
1002
- configuredPromise.catch(() => {});
1003
- };
1004
- const scheduleConfiguration = (devServerInstance) => {
1005
- const httpServer = devServerInstance?.server || devServerInstance?.httpServer || devServerInstance?.listeningApp;
1006
- if (httpServer && typeof httpServer.once === "function") if (httpServer.listening) runConfiguration(devServerInstance);
1007
- else httpServer.once("listening", () => runConfiguration(devServerInstance));
1008
- else runConfiguration(devServerInstance);
1009
- };
1010
- const originalSetupMiddlewares = devServerOptions.setupMiddlewares;
1011
- devServerOptions.setupMiddlewares = function(middlewares, devServer) {
1012
- scheduleConfiguration(devServer);
1013
- if (typeof originalSetupMiddlewares === "function") return originalSetupMiddlewares.call(this, middlewares, devServer);
1014
- return middlewares;
1015
- };
1016
- const originalOnListening = devServerOptions.onListening;
1017
- devServerOptions.onListening = function(devServer) {
1018
- scheduleConfiguration(devServer);
1019
- if (typeof originalOnListening === "function") return originalOnListening.call(this, devServer);
1020
- };
1021
- };
1022
- return {
1023
- name: PLUGIN_NAME,
1024
- enforce: "pre",
1025
- resolveId(id) {
1026
- if (id === VIRTUAL_MODULE_ID) {
1027
- debugLog("resolveId called for", id);
1028
- return id;
1029
- }
1030
- },
1031
- loadInclude(id) {
1032
- return id === VIRTUAL_MODULE_ID;
1033
- },
1034
- async load(id) {
1035
- if (id === VIRTUAL_MODULE_ID) {
1036
- const url = await globalState.tunnelUrl;
1037
- return `export function getTunnelUrl() { return ${JSON.stringify(url || "")}; }`;
1038
- }
1039
- },
1040
- vite: {
1041
- config: (config) => {
1042
- announceConnecting();
1043
- if (!config.server) config.server = {};
1044
- ensureViteAllowedHosts(config.server);
1045
- if (!isQuickMode) debugLog(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
1046
- },
1047
- configureServer: (server) => {
1048
- const configuredPromise = configureServer(server);
1049
- globalState.tunnelUrl = configuredPromise.then(() => tunnelUrl).catch(() => "");
1050
- return async () => {
1051
- await configuredPromise;
1052
- };
1053
- }
1054
- },
1055
- rspack: (compiler) => {
1056
- setupWebpackLikeDevServerIntegration(compiler, "rspack");
1057
- },
1058
- webpack: (compiler) => {
1059
- setupWebpackLikeDevServerIntegration(compiler, "webpack");
1060
- },
1061
- closeBundle() {
1062
- killCloudflared("SIGTERM");
1063
- delete globalState.child;
1064
- delete globalState.configHash;
1065
- delete globalState.shuttingDown;
1066
- }
1067
- };
1068
- };
1069
- function normalizeAddress(address) {
1070
- if (address && typeof address === "object") return {
1071
- host: "address" in address && address.address ? address.address : "localhost",
1072
- port: "port" in address && typeof address.port === "number" ? address.port : void 0
1073
- };
1074
- return { host: "localhost" };
1075
- }
1076
- async function ensureCloudflaredBinary(binPath) {
1077
- try {
1078
- await NodeFS.access(binPath);
1079
- } catch {
1080
- console.log("[unplugin-cloudflare-tunnel] Installing cloudflared binary...");
1081
- await install(binPath);
1082
- }
1083
- }
1084
- function getLocalTarget(host, port) {
1085
- return `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
1086
- }
1087
- const CloudflareTunnel = createUnplugin(unpluginFactory);
1088
- //#endregion
1
+ import { t as CloudflareTunnel } from "./src-D0eR3kCb.mjs";
1089
2
  export { CloudflareTunnel, CloudflareTunnel as default };