unplugin-cloudflare-tunnel 0.0.0-alpha-1 → 0.0.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.
@@ -1,56 +1,104 @@
1
- import { bin, install } from "cloudflared";
2
- import fs from "node:fs/promises";
3
- import { exec, spawn } from "node:child_process";
4
- import { z } from "zod";
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-CwcXCIyR.mjs";
5
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
6
 
7
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";
8
20
  const INFO_LOG_REGEX = /^.*Z INF .*/;
9
- const CloudflareErrorSchema = z.object({
10
- code: z.number(),
11
- message: z.string()
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_ESCAPE = String.fromCharCode(27);
46
+ const ANSI_STYLE_SEQUENCE_REGEX = new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, "g");
47
+ function stripAnsi(text) {
48
+ return text.replace(ANSI_STYLE_SEQUENCE_REGEX, "");
49
+ }
50
+ function colorize(text, ansi) {
51
+ if (!supportsColor()) return text;
52
+ return `${ansi}${text}${ANSI.reset}`;
53
+ }
54
+ const CloudflareErrorSchema = object({
55
+ code: number(),
56
+ message: string()
12
57
  });
13
- const CloudflareApiResponseSchema = z.object({
14
- success: z.boolean(),
15
- errors: z.array(CloudflareErrorSchema).optional(),
16
- messages: z.array(z.string()).optional(),
17
- result: z.unknown()
58
+ const CloudflareApiResponseSchema = object({
59
+ success: boolean(),
60
+ errors: optional(array(CloudflareErrorSchema)),
61
+ messages: optional(array(string())),
62
+ result: unknown()
18
63
  });
19
- const AccountSchema = z.object({
20
- id: z.string(),
21
- name: z.string()
64
+ const AccountSchema = object({
65
+ id: string(),
66
+ name: string()
22
67
  });
23
- const ZoneSchema = z.object({
24
- id: z.string(),
25
- name: z.string()
68
+ const ZoneSchema = object({
69
+ id: string(),
70
+ name: string()
26
71
  });
27
- const TunnelSchema = z.object({
28
- id: z.string(),
29
- name: z.string(),
30
- account_tag: z.string(),
31
- created_at: z.string(),
32
- connections: z.array(z.unknown()).optional()
72
+ const TunnelSchema = object({
73
+ id: string(),
74
+ name: string(),
75
+ account_tag: string(),
76
+ created_at: string(),
77
+ connections: optional(array(unknown()))
33
78
  });
34
- const DNSRecordSchema = z.object({
35
- id: z.string(),
36
- type: z.string(),
37
- name: z.string(),
38
- content: z.string(),
39
- proxied: z.boolean(),
40
- comment: z.string().nullish()
79
+ const DNSRecordSchema = object({
80
+ id: string(),
81
+ type: string(),
82
+ name: string(),
83
+ content: string(),
84
+ proxied: boolean(),
85
+ comment: nullish(string())
41
86
  });
42
87
  const unpluginFactory = (options = {}) => {
43
88
  const { enabled = true } = options;
44
89
  if (enabled === false) {
45
90
  const VIRTUAL_MODULE_ID_STUB = "virtual:unplugin-cloudflare-tunnel";
46
91
  return {
47
- name: "unplugin-cloudflare-tunnel",
92
+ name: PLUGIN_NAME,
48
93
  enforce: "pre",
49
94
  resolveId(id) {
50
- if (id === VIRTUAL_MODULE_ID_STUB) return "\0" + VIRTUAL_MODULE_ID_STUB;
95
+ if (id === VIRTUAL_MODULE_ID_STUB) return id;
96
+ },
97
+ loadInclude(id) {
98
+ return id === VIRTUAL_MODULE_ID_STUB;
51
99
  },
52
100
  load(id) {
53
- if (id === "\0" + VIRTUAL_MODULE_ID_STUB) return "export function getTunnelUrl() { return \"\"; }";
101
+ if (id === VIRTUAL_MODULE_ID_STUB) return "export function getTunnelUrl() { return \"\"; }";
54
102
  }
55
103
  };
56
104
  }
@@ -95,8 +143,79 @@ const unpluginFactory = (options = {}) => {
95
143
  cleanupConfig = namedOptions.cleanup || {};
96
144
  }
97
145
  const { port: userProvidedPort, logFile, logLevel, debug = false } = options;
98
- const debugLog = (...args) => {
99
- if (debug) console.log("[cloudflare-tunnel:debug]", ...args);
146
+ const effectivePluginLogLevel = logLevel ?? (debug ? "debug" : "info");
147
+ const pluginLog = {
148
+ debug: (...args) => {
149
+ if (debug || effectivePluginLogLevel === "debug") console.log("[cloudflare-tunnel:debug]", ...args);
150
+ },
151
+ info: (message) => {
152
+ if (shouldLog(effectivePluginLogLevel, "info")) console.log(`[unplugin-cloudflare-tunnel] ${message}`);
153
+ },
154
+ warn: (message) => {
155
+ if (shouldLog(effectivePluginLogLevel, "warn")) console.warn(`[unplugin-cloudflare-tunnel] ${message}`);
156
+ },
157
+ error: (message) => {
158
+ if (shouldLog(effectivePluginLogLevel, "error")) console.error(`[unplugin-cloudflare-tunnel] ${message}`);
159
+ }
160
+ };
161
+ const debugLog = pluginLog.debug;
162
+ const makeLocalDisplay = (localTarget) => {
163
+ if (!localTarget) return localTarget;
164
+ return localTarget.replace("http://[::1]:", "http://localhost:").replace("http://127.0.0.1:", "http://localhost:");
165
+ };
166
+ const announceConnecting = () => {
167
+ if (globalState.__tunnelConnectingAnnounced) return;
168
+ globalState.__tunnelConnectingAnnounced = true;
169
+ const message = isQuickMode ? "cf tunnel connecting…" : hostname ? `cf tunnel connecting… (${hostname})` : "cf tunnel connecting…";
170
+ console.log("");
171
+ console.log(colorize(message, ANSI.bold));
172
+ };
173
+ const announceTunnel = (params) => {
174
+ if (!params.url) return;
175
+ if (globalState.__lastAnnouncedTunnelKey === params.key) return;
176
+ globalState.__lastAnnouncedTunnelKey = params.key;
177
+ const cols = process.stdout.columns ?? 80;
178
+ const maxWidth = Math.max(10, cols - 2);
179
+ const headerText = "unplugin-cloudflare-tunnel";
180
+ const header = (() => {
181
+ return `${colorize("[", ANSI.yellow)}${headerText}${colorize("]", ANSI.yellow)}`;
182
+ })();
183
+ const urlLine = colorize(params.url, ANSI.blue + ANSI.bold);
184
+ const localLine = params.localTarget ? makeLocalDisplay(params.localTarget) : "";
185
+ const headerPlainLen = stripAnsi(header).length;
186
+ const contentPlainLen = Math.max(stripAnsi(urlLine).length, localLine.length, 10, 5);
187
+ const width = Math.min(90, maxWidth, Math.max(44, headerPlainLen, contentPlainLen + 4));
188
+ const rule = "─".repeat(width);
189
+ const center = (text) => {
190
+ const plainLen = stripAnsi(text).length;
191
+ const pad = Math.max(0, Math.floor((width - plainLen) / 2));
192
+ return `${" ".repeat(pad)}${text}`;
193
+ };
194
+ if (cols < 70) {
195
+ const out$1 = [];
196
+ out$1.push("");
197
+ out$1.push(`${header} ${colorize("Tunnel URL", ANSI.bold)} ${urlLine}`);
198
+ if (localLine) out$1.push(`${header} ${colorize("Local", ANSI.dim + ANSI.bold)} ${localLine}`);
199
+ out$1.push("");
200
+ console.log(out$1.join("\n"));
201
+ return;
202
+ }
203
+ const out = [];
204
+ out.push("");
205
+ out.push(center(header));
206
+ out.push(rule);
207
+ out.push(center(colorize("Tunnel URL", ANSI.bold)));
208
+ out.push(center(urlLine));
209
+ out.push(center(urlLine));
210
+ out.push(center(urlLine));
211
+ if (localLine) {
212
+ out.push("");
213
+ out.push(center(colorize("Local", ANSI.dim + ANSI.bold)));
214
+ out.push(center(localLine));
215
+ }
216
+ out.push(rule);
217
+ out.push("");
218
+ console.log(out.join("\n"));
100
219
  };
101
220
  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");
102
221
  let tunnelUrl = hostname ? `https://${hostname}` : "";
@@ -130,7 +249,7 @@ const unpluginFactory = (options = {}) => {
130
249
  };
131
250
  const findMismatchedSslCertificates = async (apiToken, zoneId, currentTunnelName, currentHostname) => {
132
251
  try {
133
- const certPacks = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, z.any());
252
+ const certPacks = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, any());
134
253
  const currentTunnelCerts = (Array.isArray(certPacks) ? certPacks : certPacks.result || []).filter((cert) => {
135
254
  return (cert.hostnames || cert.hosts || []).some((host) => host.startsWith(`cf-tunnel-plugin-${currentTunnelName}--`));
136
255
  });
@@ -154,7 +273,7 @@ const unpluginFactory = (options = {}) => {
154
273
  };
155
274
  const cleanupMismatchedDnsRecords = async (apiToken, zoneId, dnsComment, currentHostname, tunnelId) => {
156
275
  try {
157
- const pluginDnsRecords = await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?comment=${dnsComment}&match=all`, void 0, z.array(DNSRecordSchema));
276
+ const pluginDnsRecords = await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?comment=${dnsComment}&match=all`, void 0, array(DNSRecordSchema));
158
277
  debugLog(`Found ${pluginDnsRecords.length} DNS records for current tunnel: ${dnsComment}`);
159
278
  const expectedCnameContent = `${tunnelId}.cfargotunnel.com`;
160
279
  const mismatchedRecords = pluginDnsRecords.filter((record) => {
@@ -252,7 +371,7 @@ const unpluginFactory = (options = {}) => {
252
371
  if (logFile) cloudflaredArgs.push("--logfile", logFile);
253
372
  cloudflaredArgs.push("--url", localTarget);
254
373
  debugLog("Spawning quick tunnel:", bin, cloudflaredArgs);
255
- const child$1 = spawn(bin, cloudflaredArgs, {
374
+ const child$1 = NodeChildProcess.spawn(bin, cloudflaredArgs, {
256
375
  stdio: [
257
376
  "ignore",
258
377
  "pipe",
@@ -262,7 +381,7 @@ const unpluginFactory = (options = {}) => {
262
381
  windowsHide: true,
263
382
  shell: process.platform === "win32"
264
383
  });
265
- console.log(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child$1.pid}`);
384
+ debugLog(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child$1.pid}`);
266
385
  return new Promise((resolve, reject) => {
267
386
  let urlFound = false;
268
387
  const timeout = setTimeout(() => {
@@ -310,17 +429,17 @@ const unpluginFactory = (options = {}) => {
310
429
  globalState.shuttingDown = true;
311
430
  globalState.tunnelUrl = void 0;
312
431
  try {
313
- console.log(`[unplugin-cloudflare-tunnel] 🛑 Terminating cloudflared process (PID: ${child.pid}) with ${signal}...`);
314
- if (!child.kill(signal) && process.platform === "win32") exec(`taskkill /pid ${child.pid} /T /F`, () => {});
432
+ debugLog(`[unplugin-cloudflare-tunnel] Terminating cloudflared process (PID: ${child.pid}) with ${signal}...`);
433
+ if (!child.kill(signal) && process.platform === "win32") NodeChildProcess.exec(`taskkill /pid ${child.pid} /T /F`, () => {});
315
434
  if (signal === "SIGTERM") setTimeout(() => {
316
435
  if (child && !child.killed) {
317
- console.log("[unplugin-cloudflare-tunnel] 🛑 Force killing cloudflared process...");
318
- if (process.platform === "win32") exec(`taskkill /pid ${child.pid} /T /F`, () => {});
436
+ debugLog("[unplugin-cloudflare-tunnel] Force killing cloudflared process...");
437
+ if (process.platform === "win32") NodeChildProcess.exec(`taskkill /pid ${child.pid} /T /F`, () => {});
319
438
  else child.kill("SIGKILL");
320
439
  }
321
440
  }, 2e3);
322
441
  } catch (error) {
323
- console.log(`[unplugin-cloudflare-tunnel] Note: Error killing cloudflared: ${error}`);
442
+ debugLog(`[unplugin-cloudflare-tunnel] Note: Error killing cloudflared: ${error}`);
324
443
  }
325
444
  };
326
445
  let exitHandlersRegistered = globalState.exitHandlersRegistered ?? false;
@@ -359,9 +478,27 @@ const unpluginFactory = (options = {}) => {
359
478
  const generateDnsComment = () => {
360
479
  return `unplugin-cloudflare-tunnel:${tunnelName}`;
361
480
  };
481
+ const registerListeningHandler = (handler) => {
482
+ const httpServer = server.httpServer;
483
+ if (!httpServer) return;
484
+ const invokeHandler = () => {
485
+ try {
486
+ const maybePromise = handler();
487
+ if (maybePromise && typeof maybePromise.then === "function") maybePromise.catch((error) => {
488
+ console.error(`[unplugin-cloudflare-tunnel] ❌ Dev server listening hook failed: ${error.message}`);
489
+ });
490
+ } catch (error) {
491
+ console.error(`[unplugin-cloudflare-tunnel] ❌ Dev server listening hook failed: ${error.message}`);
492
+ }
493
+ };
494
+ httpServer.on("listening", invokeHandler);
495
+ if (httpServer.listening) invokeHandler();
496
+ };
362
497
  try {
363
498
  const { host: serverHost, port: detectedPort } = normalizeAddress(server.httpServer?.address());
364
- const port = userProvidedPort || detectedPort || server.config.server.port || 5173;
499
+ const configPortValue = server.config?.server?.port;
500
+ const resolvedConfigPort = typeof configPortValue === "string" ? Number.parseInt(configPortValue, 10) : configPortValue;
501
+ const port = userProvidedPort || detectedPort || (typeof resolvedConfigPort === "number" && !Number.isNaN(resolvedConfigPort) ? resolvedConfigPort : void 0) || 5173;
365
502
  const newConfigHash = JSON.stringify({
366
503
  isQuickMode,
367
504
  hostname,
@@ -372,13 +509,13 @@ const unpluginFactory = (options = {}) => {
372
509
  });
373
510
  if (globalState.child && !globalState.child.killed && globalState.configHash === newConfigHash) {
374
511
  tunnelUrl = await globalState.tunnelUrl ?? "";
375
- console.log("[unplugin-cloudflare-tunnel] Config unchanged – re-using existing tunnel");
512
+ debugLog("[unplugin-cloudflare-tunnel] Config unchanged – re-using existing tunnel");
376
513
  globalState.shuttingDown = false;
377
514
  registerExitHandler();
378
515
  return;
379
516
  }
380
517
  if (globalState.child && !globalState.child.killed) {
381
- console.log("[unplugin-cloudflare-tunnel] Config changed – terminating previous tunnel...");
518
+ debugLog("[unplugin-cloudflare-tunnel] Config changed – terminating previous tunnel...");
382
519
  try {
383
520
  globalState.child.kill("SIGTERM");
384
521
  } catch (_) {}
@@ -387,7 +524,7 @@ const unpluginFactory = (options = {}) => {
387
524
  delete globalState.configHash;
388
525
  globalState.shuttingDown = false;
389
526
  if (isQuickMode) {
390
- console.log("[unplugin-cloudflare-tunnel] Starting quick tunnel mode...");
527
+ debugLog("[unplugin-cloudflare-tunnel] Starting quick tunnel mode...");
391
528
  debugLog("Quick tunnel mode - no API token or hostname required");
392
529
  await ensureCloudflaredBinary(bin);
393
530
  const localTarget$1 = getLocalTarget(serverHost, port);
@@ -399,20 +536,33 @@ const unpluginFactory = (options = {}) => {
399
536
  globalState.child = child;
400
537
  globalState.configHash = newConfigHash;
401
538
  registerExitHandler();
402
- console.log(`🌐 Quick tunnel ready at: ${url}`);
403
- server.httpServer?.on("listening", async () => {
539
+ registerListeningHandler(() => {
540
+ const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
541
+ const actualLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
542
+ announceTunnel({
543
+ key: `quick:${url}:${actualPort ?? port}`,
544
+ url,
545
+ localTarget: actualLocalTarget
546
+ });
547
+ });
548
+ registerListeningHandler(async () => {
404
549
  try {
405
550
  const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
406
551
  if (actualPort !== port) {
407
- console.log(`[unplugin-cloudflare-tunnel] ⚠️ Port conflict detected - server is using port ${actualPort} instead of ${port}`);
408
- console.log(`[unplugin-cloudflare-tunnel] 🔄 Quick tunnel needs to be restarted for new port...`);
552
+ pluginLog.warn(`Port conflict detected - server is using port ${actualPort} instead of ${port}`);
553
+ pluginLog.info("Restarting quick tunnel for the new port...");
409
554
  killCloudflared("SIGTERM");
410
555
  await new Promise((resolve) => setTimeout(resolve, 1e3));
411
- const { child: newChild, url: newUrl } = await spawnQuickTunnel(getLocalTarget(actualServerHost, actualPort ?? port));
556
+ const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
557
+ const { child: newChild, url: newUrl } = await spawnQuickTunnel(newLocalTarget);
412
558
  tunnelUrl = newUrl;
413
559
  child = newChild;
414
560
  globalState.child = child;
415
- console.log(`🌐 Quick tunnel updated for port ${actualPort}: ${newUrl}`);
561
+ announceTunnel({
562
+ key: `quick:${newUrl}:${actualPort ?? port}`,
563
+ url: newUrl,
564
+ localTarget: newLocalTarget
565
+ });
416
566
  globalState.configHash = JSON.stringify({
417
567
  isQuickMode,
418
568
  hostname,
@@ -435,12 +585,12 @@ const unpluginFactory = (options = {}) => {
435
585
  throw error;
436
586
  }
437
587
  }
438
- console.log("[unplugin-cloudflare-tunnel] Starting named tunnel mode...");
588
+ debugLog("[unplugin-cloudflare-tunnel] Starting named tunnel mode...");
439
589
  const apiToken = providedApiToken || process.env.CLOUDFLARE_API_KEY;
440
590
  if (!apiToken) throw new Error("[unplugin-cloudflare-tunnel] API token is required. Provide it via 'apiToken' option or set the CLOUDFLARE_API_KEY environment variable. Get your token at: https://dash.cloudflare.com/profile/api-tokens");
441
- console.log(`[unplugin-cloudflare-tunnel] Using port ${port}${userProvidedPort === port ? " (user-provided)" : " (from bundler config)"}`);
591
+ debugLog(`[unplugin-cloudflare-tunnel] Using port ${port}${userProvidedPort === port ? " (user-provided)" : " (from bundler config)"}`);
442
592
  await ensureCloudflaredBinary(bin);
443
- const accounts = await cf(apiToken, "GET", "/accounts", void 0, z.array(AccountSchema));
593
+ const accounts = await cf(apiToken, "GET", "/accounts", void 0, array(AccountSchema));
444
594
  const accountId = forcedAccount || accounts[0]?.id;
445
595
  if (!accountId) throw new Error("Unable to determine Cloudflare account ID");
446
596
  const apexDomain = hostname.split(".").slice(-2).join(".");
@@ -451,18 +601,18 @@ const unpluginFactory = (options = {}) => {
451
601
  if (!zoneId) {
452
602
  let zones = [];
453
603
  try {
454
- zones = await cf(apiToken, "GET", `/zones?name=${parentDomain}`, void 0, z.array(ZoneSchema));
604
+ zones = await cf(apiToken, "GET", `/zones?name=${parentDomain}`, void 0, array(ZoneSchema));
455
605
  } catch (error) {
456
606
  debugLog("← Error fetching zone for parent domain", error);
457
607
  }
458
- if (zones.length === 0) zones = await cf(apiToken, "GET", `/zones?name=${apexDomain}`, void 0, z.array(ZoneSchema));
608
+ if (zones.length === 0) zones = await cf(apiToken, "GET", `/zones?name=${apexDomain}`, void 0, array(ZoneSchema));
459
609
  zoneId = zones[0]?.id;
460
610
  }
461
611
  if (!zoneId) throw new Error(`Zone ${apexDomain} not found in account ${accountId}`);
462
612
  const { autoCleanup = true } = cleanupConfig;
463
- let tunnel = (await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel?name=${tunnelName}`, void 0, z.array(TunnelSchema)))[0];
613
+ let tunnel = (await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel?name=${tunnelName}`, void 0, array(TunnelSchema)))[0];
464
614
  if (!tunnel) {
465
- console.log(`[unplugin-cloudflare-tunnel] Creating tunnel '${tunnelName}'...`);
615
+ pluginLog.info(`Creating tunnel '${tunnelName}'...`);
466
616
  tunnel = await cf(apiToken, "POST", `/accounts/${accountId}/cfd_tunnel`, {
467
617
  name: tunnelName,
468
618
  config_src: "cloudflare"
@@ -470,13 +620,13 @@ const unpluginFactory = (options = {}) => {
470
620
  }
471
621
  const tunnelId = tunnel.id;
472
622
  if (autoCleanup) {
473
- console.log(`[unplugin-cloudflare-tunnel] 🧹 Running resource cleanup for tunnel '${tunnelName}'...`);
623
+ debugLog(`[unplugin-cloudflare-tunnel] Running resource cleanup for tunnel '${tunnelName}'...`);
474
624
  const dnsCleanup = await cleanupMismatchedDnsRecords(apiToken, zoneId, generateDnsComment(), hostname, tunnelId);
475
- if (dnsCleanup.found.length > 0) console.log(`[unplugin-cloudflare-tunnel] 📊 DNS cleanup: ${dnsCleanup.found.length} mismatched, ${dnsCleanup.deleted.length} deleted`);
625
+ if (dnsCleanup.found.length > 0) pluginLog.warn(`DNS cleanup: ${dnsCleanup.found.length} mismatched, ${dnsCleanup.deleted.length} deleted`);
476
626
  const mismatchedSslCerts = await findMismatchedSslCertificates(apiToken, zoneId, tunnelName, hostname);
477
627
  if (mismatchedSslCerts.length > 0) {
478
628
  for (const cert of mismatchedSslCerts) await cf(apiToken, "DELETE", `/zones/${zoneId}/ssl/certificate_packs/${cert.id}`);
479
- console.log(`[unplugin-cloudflare-tunnel] 📊 SSL cleanup: ${mismatchedSslCerts.length} deleted`);
629
+ pluginLog.warn(`SSL cleanup: ${mismatchedSslCerts.length} deleted`);
480
630
  }
481
631
  } else debugLog("← Cleanup skipped", cleanupConfig);
482
632
  const localTarget = getLocalTarget(serverHost, port);
@@ -490,7 +640,7 @@ const unpluginFactory = (options = {}) => {
490
640
  };
491
641
  if (dnsOption) {
492
642
  const ensureDnsRecord = async (type, content) => {
493
- if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(dnsOption)}`, void 0, z.array(DNSRecordSchema))).length === 0) {
643
+ if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(dnsOption)}`, void 0, array(DNSRecordSchema))).length === 0) {
494
644
  console.log(`[unplugin-cloudflare-tunnel] Creating ${type} record for ${dnsOption}...`);
495
645
  await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
496
646
  type,
@@ -504,22 +654,34 @@ const unpluginFactory = (options = {}) => {
504
654
  await ensureDnsRecord("CNAME", `${tunnelId}.cfargotunnel.com`);
505
655
  } else {
506
656
  const wildcardDns = `*.${parentDomain}`;
507
- if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${wildcardDns}`, void 0, z.array(DNSRecordSchema))).length === 0) {
508
- if (!((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${hostname}`, void 0, z.array(DNSRecordSchema))).length > 0)) {
657
+ if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${wildcardDns}`, void 0, array(DNSRecordSchema))).length === 0) {
658
+ const existingRecord = (await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${hostname}`, void 0, array(DNSRecordSchema)))[0];
659
+ const expectedContent = `${tunnelId}.cfargotunnel.com`;
660
+ if (!existingRecord) {
509
661
  console.log(`[unplugin-cloudflare-tunnel] Creating DNS record for ${hostname}...`);
510
662
  await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
511
663
  type: "CNAME",
512
664
  name: hostname,
513
- content: `${tunnelId}.cfargotunnel.com`,
665
+ content: expectedContent,
666
+ proxied: true,
667
+ comment: generateDnsComment()
668
+ }, DNSRecordSchema);
669
+ } else if (existingRecord.content !== expectedContent) {
670
+ debugLog(`← DNS record for ${hostname} points to different tunnel, updating...`);
671
+ pluginLog.info(`Updating DNS record for ${hostname} to point to tunnel '${tunnelName}'...`);
672
+ await cf(apiToken, "PUT", `/zones/${zoneId}/dns_records/${existingRecord.id}`, {
673
+ type: "CNAME",
674
+ name: hostname,
675
+ content: expectedContent,
514
676
  proxied: true,
515
677
  comment: generateDnsComment()
516
678
  }, DNSRecordSchema);
517
679
  }
518
680
  }
519
681
  }
520
- const token = await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`, void 0, z.string());
682
+ const token = await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`, void 0, string());
521
683
  try {
522
- const certListRaw = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, z.any());
684
+ const certListRaw = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, any());
523
685
  const certPacks = Array.isArray(certListRaw) ? certListRaw : certListRaw.result || [];
524
686
  const certContainingHost = (host) => certPacks.filter((c) => (c.hostnames || c.hosts || []).includes(host))?.[0];
525
687
  if (sslOption) {
@@ -545,11 +707,11 @@ const unpluginFactory = (options = {}) => {
545
707
  const wildcardDomain = `*.${parentDomain}`;
546
708
  const wildcardExists = certContainingHost(wildcardDomain);
547
709
  if (!wildcardExists) {
548
- const totalTls = await cf(apiToken, "GET", `/zones/${zoneId}/acm/total_tls`, void 0, z.object({ status: z.string() }));
710
+ const totalTls = await cf(apiToken, "GET", `/zones/${zoneId}/acm/total_tls`, void 0, object({ status: string() }));
549
711
  debugLog("← Total TLS", totalTls);
550
712
  const existingHostnameCert = certContainingHost(hostname);
551
713
  if (totalTls.status !== "on" && !existingHostnameCert) {
552
- console.log(`[unplugin-cloudflare-tunnel] Requesting edge certificate for ${hostname}...`);
714
+ pluginLog.info(`Requesting edge certificate for ${hostname}...`);
553
715
  const tagHostname = generateSslTagHostname();
554
716
  const certificateHosts = [hostname, tagHostname];
555
717
  debugLog(`Adding tag hostname to certificate: ${tagHostname}`);
@@ -574,7 +736,7 @@ const unpluginFactory = (options = {}) => {
574
736
  if (logFile) cloudflaredArgs.push("--logfile", logFile);
575
737
  debugLog("Spawning cloudflared", bin, cloudflaredArgs);
576
738
  cloudflaredArgs.push("run", "--token", token);
577
- child = spawn(bin, cloudflaredArgs, {
739
+ child = NodeChildProcess.spawn(bin, cloudflaredArgs, {
578
740
  stdio: [
579
741
  "ignore",
580
742
  "pipe",
@@ -584,18 +746,37 @@ const unpluginFactory = (options = {}) => {
584
746
  windowsHide: true,
585
747
  shell: process.platform === "win32"
586
748
  });
587
- console.log(`[unplugin-cloudflare-tunnel] Process spawned with PID: ${child.pid}`);
749
+ debugLog(`[unplugin-cloudflare-tunnel] Process spawned with PID: ${child.pid}`);
588
750
  globalState.child = child;
589
751
  globalState.configHash = newConfigHash;
590
752
  registerExitHandler();
591
753
  let tunnelReady = false;
754
+ const logCloudflaredLines = (kind, text) => {
755
+ if (globalState.shuttingDown && !debug) return;
756
+ const isVerbose = effectiveLogLevel === "debug" || effectiveLogLevel === "info";
757
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
758
+ if (isVerbose) {
759
+ for (const line of lines) {
760
+ const prefix = kind === "stdout" ? "[cloudflared stdout]" : "[cloudflared stderr]";
761
+ if (kind === "stdout") console.log(`${prefix} ${line}`);
762
+ else console.error(`${prefix} ${line}`);
763
+ }
764
+ return;
765
+ }
766
+ for (const line of lines) {
767
+ if (INFO_LOG_REGEX.test(line)) continue;
768
+ const prefix = kind === "stdout" ? "[cloudflared stdout]" : "[cloudflared stderr]";
769
+ if (kind === "stdout") console.log(`${prefix} ${line}`);
770
+ else console.error(`${prefix} ${line}`);
771
+ }
772
+ };
592
773
  child.stdout?.on("data", (data) => {
593
774
  const output = data.toString();
594
- if (!globalState.shuttingDown || debug) console.log(`[cloudflared stdout] ${output.trim()}`);
775
+ logCloudflaredLines("stdout", output);
595
776
  if (output.includes("Connection") && output.includes("registered")) {
596
777
  if (!tunnelReady) {
597
778
  tunnelReady = true;
598
- console.log(`🌐 Cloudflare tunnel started for https://${hostname}`);
779
+ pluginLog.info(`Tunnel connected for https://${hostname}`);
599
780
  }
600
781
  }
601
782
  });
@@ -605,10 +786,7 @@ const unpluginFactory = (options = {}) => {
605
786
  if (logLevel === "debug") console.log(`[cloudflared debug] ${error}`);
606
787
  return;
607
788
  }
608
- if (!globalState.shuttingDown || debug) console.error(`[cloudflared stderr] ${error}`);
609
- if (error.toLowerCase().includes("error") || error.toLowerCase().includes("failed") || error.toLowerCase().includes("fatal")) {
610
- if (!globalState.shuttingDown || debug) console.error(`[unplugin-cloudflare-tunnel] ⚠️ ${error}`);
611
- }
789
+ logCloudflaredLines("stderr", error);
612
790
  });
613
791
  child.on("error", (error) => {
614
792
  console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to start tunnel process: ${error.message}`);
@@ -620,25 +798,31 @@ const unpluginFactory = (options = {}) => {
620
798
  if (signal) console.error(`[unplugin-cloudflare-tunnel] Process terminated by signal: ${signal}`);
621
799
  } else if (code === 0) console.log(`[unplugin-cloudflare-tunnel] ✅ Tunnel process exited cleanly`);
622
800
  });
623
- setTimeout(() => {
624
- if (!tunnelReady) console.log(`🌐 Cloudflare tunnel starting for https://${hostname}`);
625
- }, 3e3);
801
+ registerListeningHandler(() => {
802
+ const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
803
+ const actualLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
804
+ announceTunnel({
805
+ key: `named:${hostname}:${actualPort ?? port}`,
806
+ url: `https://${hostname}`,
807
+ localTarget: actualLocalTarget
808
+ });
809
+ });
626
810
  server.httpServer?.once("close", () => {
627
811
  killCloudflared("SIGTERM");
628
812
  });
629
- server.httpServer?.on("listening", async () => {
813
+ registerListeningHandler(async () => {
630
814
  try {
631
815
  const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
632
816
  if (actualPort !== port) {
633
- console.log(`[unplugin-cloudflare-tunnel] ⚠️ Port conflict detected - server is using port ${actualPort} instead of ${port}`);
634
- console.log(`[unplugin-cloudflare-tunnel] 🔄 Updating tunnel configuration...`);
817
+ pluginLog.warn(`Port conflict detected - server is using port ${actualPort} instead of ${port}`);
818
+ pluginLog.info("Updating tunnel configuration...");
635
819
  const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
636
820
  debugLog("← Updating local target to", newLocalTarget);
637
821
  await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
638
822
  hostname,
639
823
  service: newLocalTarget
640
824
  }, { service: "http_status:404" }] } });
641
- console.log(`[unplugin-cloudflare-tunnel] ✅ Tunnel configuration updated to use port ${actualPort}`);
825
+ pluginLog.info(`Tunnel configuration updated to use port ${actualPort}`);
642
826
  globalState.configHash = JSON.stringify({
643
827
  hostname,
644
828
  port: actualPort,
@@ -652,27 +836,126 @@ const unpluginFactory = (options = {}) => {
652
836
  }
653
837
  });
654
838
  } catch (error) {
655
- console.error(`[unplugin-cloudflare-tunnel] Setup failed: ${error.message}`);
656
- if (error.message.includes("API token")) {
657
- console.error(`[unplugin-cloudflare-tunnel] 💡 Check your API token at: https://dash.cloudflare.com/profile/api-tokens`);
658
- console.error(`[unplugin-cloudflare-tunnel] 💡 Required permissions: Zone:Zone:Read, Zone:DNS:Edit, Account:Cloudflare Tunnel:Edit`);
659
- } 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`);
660
- else if (error.message.includes("cloudflared")) console.error(`[unplugin-cloudflare-tunnel] 💡 Try deleting node_modules and reinstalling to get a fresh cloudflared binary`);
839
+ if (error instanceof Error) {
840
+ console.error(`[unplugin-cloudflare-tunnel] ❌ Setup failed: ${error.message}`);
841
+ if (error.message.includes("API token")) {
842
+ console.error(`[unplugin-cloudflare-tunnel] 💡 Check your API token at: https://dash.cloudflare.com/profile/api-tokens`);
843
+ console.error(`[unplugin-cloudflare-tunnel] 💡 Required permissions: Zone:Zone:Read, Zone:DNS:Edit, Account:Cloudflare Tunnel:Edit`);
844
+ } 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`);
845
+ else if (error.message.includes("cloudflared")) console.error(`[unplugin-cloudflare-tunnel] 💡 Try deleting node_modules and reinstalling to get a fresh cloudflared binary`);
846
+ }
661
847
  throw error;
662
848
  }
663
849
  };
850
+ const ensureWebpackAllowedHosts = (devServerOptions, bundler) => {
851
+ if (!devServerOptions) return;
852
+ const hostToAllow = isQuickMode ? ".trycloudflare.com" : hostname;
853
+ if (!hostToAllow) return;
854
+ const label = bundler === "webpack" ? "Webpack" : "Rspack";
855
+ const normalizeArray = (values) => {
856
+ const unique = Array.from(new Set(values.filter(Boolean)));
857
+ devServerOptions.allowedHosts = unique;
858
+ return unique;
859
+ };
860
+ let modified = false;
861
+ const current = devServerOptions.allowedHosts;
862
+ if (current === "all") return;
863
+ if (typeof current === "undefined" || current === "auto") {
864
+ normalizeArray(["localhost", hostToAllow]);
865
+ modified = true;
866
+ } else if (typeof current === "string") {
867
+ if (current !== hostToAllow) {
868
+ normalizeArray([current, hostToAllow]);
869
+ modified = true;
870
+ }
871
+ } else if (Array.isArray(current)) {
872
+ if (!current.includes(hostToAllow)) {
873
+ current.push(hostToAllow);
874
+ modified = true;
875
+ }
876
+ }
877
+ if (modified) debugLog(`[unplugin-cloudflare-tunnel] Configured ${label} devServer.allowedHosts to include ${hostToAllow}`);
878
+ };
879
+ const setupWebpackLikeDevServerIntegration = (compiler, bundler) => {
880
+ if ((compiler?.options?.mode ?? process.env.NODE_ENV) === "production") return;
881
+ const optionsContainer = compiler.options;
882
+ if (!optionsContainer.devServer) optionsContainer.devServer = {};
883
+ const devServerOptions = optionsContainer.devServer;
884
+ ensureWebpackAllowedHosts(devServerOptions, bundler);
885
+ let lastHttpServer;
886
+ let missingServerWarned = false;
887
+ const runConfiguration = (devServerInstance) => {
888
+ if (!devServerInstance) {
889
+ if (!missingServerWarned) {
890
+ console.warn(`[unplugin-cloudflare-tunnel] ${bundler} dev server instance unavailable; skipping tunnel setup`);
891
+ missingServerWarned = true;
892
+ }
893
+ return;
894
+ }
895
+ const httpServer = [
896
+ devServerInstance.server,
897
+ devServerInstance.httpServer,
898
+ devServerInstance.listeningApp,
899
+ devServerInstance.server?.server
900
+ ].find((candidate) => candidate);
901
+ if (!httpServer) {
902
+ if (!missingServerWarned) {
903
+ console.warn(`[unplugin-cloudflare-tunnel] Unable to locate HTTP server from ${bundler} dev server; tunnel will not start`);
904
+ missingServerWarned = true;
905
+ }
906
+ return;
907
+ }
908
+ if (lastHttpServer === httpServer) return;
909
+ lastHttpServer = httpServer;
910
+ httpServer.once("close", () => {
911
+ if (lastHttpServer === httpServer) lastHttpServer = void 0;
912
+ });
913
+ const configuredPromise = configureServer({
914
+ httpServer,
915
+ config: { server: { port: devServerInstance.options?.port ?? devServerOptions?.port } }
916
+ });
917
+ globalState.tunnelUrl = configuredPromise.then(() => tunnelUrl).catch(() => "");
918
+ configuredPromise.catch(() => {});
919
+ };
920
+ const scheduleConfiguration = (devServerInstance) => {
921
+ const httpServer = devServerInstance?.server || devServerInstance?.httpServer || devServerInstance?.listeningApp;
922
+ if (httpServer && typeof httpServer.once === "function") if (httpServer.listening) runConfiguration(devServerInstance);
923
+ else httpServer.once("listening", () => runConfiguration(devServerInstance));
924
+ else runConfiguration(devServerInstance);
925
+ };
926
+ const originalSetupMiddlewares = devServerOptions.setupMiddlewares;
927
+ devServerOptions.setupMiddlewares = function(middlewares, devServer) {
928
+ scheduleConfiguration(devServer);
929
+ if (typeof originalSetupMiddlewares === "function") return originalSetupMiddlewares.call(this, middlewares, devServer);
930
+ return middlewares;
931
+ };
932
+ const originalOnListening = devServerOptions.onListening;
933
+ devServerOptions.onListening = function(devServer) {
934
+ scheduleConfiguration(devServer);
935
+ if (typeof originalOnListening === "function") return originalOnListening.call(this, devServer);
936
+ };
937
+ };
664
938
  return {
665
- name: "unplugin-cloudflare-tunnel",
939
+ name: PLUGIN_NAME,
666
940
  enforce: "pre",
667
941
  resolveId(id) {
668
- if (id === VIRTUAL_MODULE_ID) return "\0" + VIRTUAL_MODULE_ID;
942
+ if (id === VIRTUAL_MODULE_ID) {
943
+ debugLog("resolveId called for", id);
944
+ return id;
945
+ }
946
+ },
947
+ loadInclude(id) {
948
+ return id === VIRTUAL_MODULE_ID;
669
949
  },
670
950
  async load(id) {
671
- const url = await globalState.tunnelUrl;
672
- if (id === "\0" + VIRTUAL_MODULE_ID) return `export function getTunnelUrl() { return ${JSON.stringify(url || "")}; }`;
951
+ if (id === VIRTUAL_MODULE_ID) {
952
+ const url = await globalState.tunnelUrl;
953
+ return `export function getTunnelUrl() { return ${JSON.stringify(url || "")}; }`;
954
+ }
673
955
  },
674
956
  vite: {
675
- config(config) {
957
+ config: (config) => {
958
+ announceConnecting();
676
959
  if (!config.server) config.server = {};
677
960
  if (isQuickMode) {
678
961
  config.server.allowedHosts = [".trycloudflare.com"];
@@ -680,15 +963,15 @@ const unpluginFactory = (options = {}) => {
680
963
  }
681
964
  if (!config.server.allowedHosts) {
682
965
  config.server.allowedHosts = [hostname];
683
- console.log(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
966
+ debugLog(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
684
967
  } else if (Array.isArray(config.server.allowedHosts)) {
685
968
  if (!config.server.allowedHosts.includes(hostname)) {
686
969
  config.server.allowedHosts.push(hostname);
687
- console.log(`[unplugin-cloudflare-tunnel] Added ${hostname} to allowed hosts`);
970
+ debugLog(`[unplugin-cloudflare-tunnel] Added ${hostname} to allowed hosts`);
688
971
  }
689
972
  }
690
973
  },
691
- configureServer(server) {
974
+ configureServer: (server) => {
692
975
  const configuredPromise = configureServer(server);
693
976
  globalState.tunnelUrl = configuredPromise.then(() => tunnelUrl).catch(() => "");
694
977
  return async () => {
@@ -696,6 +979,12 @@ const unpluginFactory = (options = {}) => {
696
979
  };
697
980
  }
698
981
  },
982
+ rspack: (compiler) => {
983
+ setupWebpackLikeDevServerIntegration(compiler, "rspack");
984
+ },
985
+ webpack: (compiler) => {
986
+ setupWebpackLikeDevServerIntegration(compiler, "webpack");
987
+ },
699
988
  closeBundle() {
700
989
  killCloudflared("SIGTERM");
701
990
  delete globalState.child;
@@ -713,7 +1002,7 @@ function normalizeAddress(address) {
713
1002
  }
714
1003
  async function ensureCloudflaredBinary(binPath) {
715
1004
  try {
716
- await fs.access(binPath);
1005
+ await NodeFS.access(binPath);
717
1006
  } catch {
718
1007
  console.log("[unplugin-cloudflare-tunnel] Installing cloudflared binary...");
719
1008
  await install(binPath);
@@ -726,4 +1015,4 @@ const CloudflareTunnel = createUnplugin(unpluginFactory);
726
1015
  var src_default = CloudflareTunnel;
727
1016
 
728
1017
  //#endregion
729
- export { src_default as n, CloudflareTunnel as t };
1018
+ export { CloudflareTunnel, src_default as default };