unplugin-cloudflare-tunnel 0.0.4 → 0.1.0
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/.github/README.md +71 -1
- package/dist/api.d.mts +40 -37
- package/dist/api.mjs +4 -5
- package/dist/astro.mjs +3 -5
- package/dist/esbuild.d.mts +2 -3
- package/dist/esbuild.mjs +1 -4
- package/dist/farm.d.mts +0 -1
- package/dist/farm.mjs +1 -4
- package/dist/index.d.mts +64 -43
- package/dist/index.mjs +185 -114
- package/dist/rolldown.d.mts +3681 -3
- package/dist/rolldown.mjs +1 -4
- package/dist/rollup.d.mts +2 -3
- package/dist/rollup.mjs +1 -4
- package/dist/rspack.d.mts +2 -3
- package/dist/rspack.mjs +1 -4
- package/dist/{schemas-CwcXCIyR.mjs → schemas-Cpk3vGGi.mjs} +46 -195
- package/dist/schemas-DKJtFAG_.d.mts +702 -0
- package/dist/virtual.d.mts +0 -1
- package/dist/vite.d.mts +2 -3
- package/dist/vite.mjs +1 -4
- package/dist/webpack.d.mts +2 -3
- package/dist/webpack.mjs +1 -4
- package/package.json +35 -23
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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-
|
|
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
2
|
import { createUnplugin } from "unplugin";
|
|
3
3
|
import NodeFS from "node:fs/promises";
|
|
4
4
|
import { bin, install } from "cloudflared";
|
|
5
5
|
import * as NodeChildProcess from "node:child_process";
|
|
6
|
-
|
|
6
|
+
import * as NodeUtil from "node:util";
|
|
7
7
|
//#region src/index.ts
|
|
8
8
|
/**
|
|
9
9
|
* @fileoverview Cloudflare Tunnel Unplugin
|
|
@@ -42,8 +42,7 @@ const ANSI = {
|
|
|
42
42
|
blue: "\x1B[34m",
|
|
43
43
|
yellow: "\x1B[33m"
|
|
44
44
|
};
|
|
45
|
-
const
|
|
46
|
-
const ANSI_STYLE_SEQUENCE_REGEX = new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, "g");
|
|
45
|
+
const ANSI_STYLE_SEQUENCE_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
47
46
|
function stripAnsi(text) {
|
|
48
47
|
return text.replace(ANSI_STYLE_SEQUENCE_REGEX, "");
|
|
49
48
|
}
|
|
@@ -67,7 +66,8 @@ const AccountSchema = object({
|
|
|
67
66
|
});
|
|
68
67
|
const ZoneSchema = object({
|
|
69
68
|
id: string(),
|
|
70
|
-
name: string()
|
|
69
|
+
name: string(),
|
|
70
|
+
account: optional(object({ id: string() }))
|
|
71
71
|
});
|
|
72
72
|
const TunnelSchema = object({
|
|
73
73
|
id: string(),
|
|
@@ -107,7 +107,11 @@ const unpluginFactory = (options = {}) => {
|
|
|
107
107
|
globalThis[GLOBAL_STATE] = globalState;
|
|
108
108
|
let child = globalState.child;
|
|
109
109
|
const VIRTUAL_MODULE_ID = "virtual:unplugin-cloudflare-tunnel";
|
|
110
|
-
const
|
|
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");
|
|
111
115
|
if (isQuickMode) {
|
|
112
116
|
const invalidOptions = [
|
|
113
117
|
"apiToken",
|
|
@@ -118,7 +122,7 @@ const unpluginFactory = (options = {}) => {
|
|
|
118
122
|
"ssl",
|
|
119
123
|
"cleanup"
|
|
120
124
|
].filter((opt) => opt in options);
|
|
121
|
-
if (invalidOptions.length > 0) throw new Error(`[unplugin-cloudflare-tunnel] The following options are only supported in named tunnel mode
|
|
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.`);
|
|
122
126
|
}
|
|
123
127
|
let providedApiToken;
|
|
124
128
|
let hostname;
|
|
@@ -142,11 +146,37 @@ const unpluginFactory = (options = {}) => {
|
|
|
142
146
|
sslOption = namedOptions.ssl;
|
|
143
147
|
cleanupConfig = namedOptions.cleanup || {};
|
|
144
148
|
}
|
|
145
|
-
const { port: userProvidedPort, logFile, logLevel, debug = false } = options;
|
|
149
|
+
const { port: userProvidedPort, logFile, logLevel, protocol = "http2", debug = false } = options;
|
|
146
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
|
+
};
|
|
147
177
|
const pluginLog = {
|
|
148
178
|
debug: (...args) => {
|
|
149
|
-
if (debug || effectivePluginLogLevel === "debug") console.log("[cloudflare-tunnel:debug]", ...args);
|
|
179
|
+
if (debug || effectivePluginLogLevel === "debug") console.log("[cloudflare-tunnel:debug]", ...args.map((arg) => formatDebugValue(arg)));
|
|
150
180
|
},
|
|
151
181
|
info: (message) => {
|
|
152
182
|
if (shouldLog(effectivePluginLogLevel, "info")) console.log(`[unplugin-cloudflare-tunnel] ${message}`);
|
|
@@ -176,10 +206,7 @@ const unpluginFactory = (options = {}) => {
|
|
|
176
206
|
globalState.__lastAnnouncedTunnelKey = params.key;
|
|
177
207
|
const cols = process.stdout.columns ?? 80;
|
|
178
208
|
const maxWidth = Math.max(10, cols - 2);
|
|
179
|
-
const
|
|
180
|
-
const header = (() => {
|
|
181
|
-
return `${colorize("[", ANSI.yellow)}${headerText}${colorize("]", ANSI.yellow)}`;
|
|
182
|
-
})();
|
|
209
|
+
const header = `${colorize("[", ANSI.yellow)}unplugin-cloudflare-tunnel${colorize("]", ANSI.yellow)}`;
|
|
183
210
|
const urlLine = colorize(params.url, ANSI.blue + ANSI.bold);
|
|
184
211
|
const localLine = params.localTarget ? makeLocalDisplay(params.localTarget) : "";
|
|
185
212
|
const headerPlainLen = stripAnsi(header).length;
|
|
@@ -192,12 +219,12 @@ const unpluginFactory = (options = {}) => {
|
|
|
192
219
|
return `${" ".repeat(pad)}${text}`;
|
|
193
220
|
};
|
|
194
221
|
if (cols < 70) {
|
|
195
|
-
const out
|
|
196
|
-
out
|
|
197
|
-
out
|
|
198
|
-
if (localLine) out
|
|
199
|
-
out
|
|
200
|
-
console.log(out
|
|
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"));
|
|
201
228
|
return;
|
|
202
229
|
}
|
|
203
230
|
const out = [];
|
|
@@ -206,8 +233,6 @@ const unpluginFactory = (options = {}) => {
|
|
|
206
233
|
out.push(rule);
|
|
207
234
|
out.push(center(colorize("Tunnel URL", ANSI.bold)));
|
|
208
235
|
out.push(center(urlLine));
|
|
209
|
-
out.push(center(urlLine));
|
|
210
|
-
out.push(center(urlLine));
|
|
211
236
|
if (localLine) {
|
|
212
237
|
out.push("");
|
|
213
238
|
out.push(center(colorize("Local", ANSI.dim + ANSI.bold)));
|
|
@@ -229,19 +254,23 @@ const unpluginFactory = (options = {}) => {
|
|
|
229
254
|
"fatal"
|
|
230
255
|
].includes(logLevel)) throw new Error("[unplugin-cloudflare-tunnel] logLevel must be one of: debug, info, warn, error, fatal");
|
|
231
256
|
const effectiveLogLevel = logLevel ?? (debug ? "info" : "warn");
|
|
232
|
-
|
|
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);
|
|
233
261
|
if (dnsOption) {
|
|
234
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");
|
|
235
263
|
}
|
|
236
264
|
if (sslOption) {
|
|
237
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");
|
|
238
266
|
}
|
|
239
|
-
|
|
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()) => {
|
|
240
269
|
const trackingKey = `ssl-cert-${certificateId}`;
|
|
241
270
|
globalState[trackingKey] = {
|
|
242
271
|
id: certificateId,
|
|
243
272
|
hosts,
|
|
244
|
-
tunnelName
|
|
273
|
+
tunnelName,
|
|
245
274
|
timestamp,
|
|
246
275
|
pluginVersion: "1.0.0"
|
|
247
276
|
};
|
|
@@ -365,13 +394,14 @@ const unpluginFactory = (options = {}) => {
|
|
|
365
394
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
366
395
|
}
|
|
367
396
|
};
|
|
368
|
-
const spawnQuickTunnel = async (localTarget) => {
|
|
397
|
+
const spawnQuickTunnel = async (localTarget, protocol) => {
|
|
369
398
|
const cloudflaredArgs = ["tunnel"];
|
|
370
399
|
cloudflaredArgs.push("--loglevel", "info");
|
|
371
400
|
if (logFile) cloudflaredArgs.push("--logfile", logFile);
|
|
401
|
+
cloudflaredArgs.push("--protocol", protocol);
|
|
372
402
|
cloudflaredArgs.push("--url", localTarget);
|
|
373
403
|
debugLog("Spawning quick tunnel:", bin, cloudflaredArgs);
|
|
374
|
-
const child
|
|
404
|
+
const child = NodeChildProcess.spawn(bin, cloudflaredArgs, {
|
|
375
405
|
stdio: [
|
|
376
406
|
"ignore",
|
|
377
407
|
"pipe",
|
|
@@ -381,27 +411,43 @@ const unpluginFactory = (options = {}) => {
|
|
|
381
411
|
windowsHide: true,
|
|
382
412
|
shell: process.platform === "win32"
|
|
383
413
|
});
|
|
384
|
-
debugLog(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child
|
|
414
|
+
debugLog(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child.pid}`);
|
|
385
415
|
return new Promise((resolve, reject) => {
|
|
386
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
|
+
};
|
|
387
428
|
const timeout = setTimeout(() => {
|
|
388
|
-
if (!urlFound)
|
|
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
|
+
}
|
|
389
435
|
}, 3e4);
|
|
390
|
-
child
|
|
436
|
+
child.stdout?.on("data", (data) => {
|
|
391
437
|
const output = data.toString();
|
|
392
438
|
if (!globalState.shuttingDown || debug) {
|
|
393
439
|
if (effectiveLogLevel === "debug" || effectiveLogLevel === "info") console.log(`[cloudflared stdout] ${output.trim()}`);
|
|
394
440
|
else for (const line of output.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.log(`[cloudflared stdout] ${line.trim()}`);
|
|
395
441
|
}
|
|
396
442
|
});
|
|
397
|
-
child
|
|
443
|
+
child.stderr?.on("data", (data) => {
|
|
398
444
|
const error = data.toString().trim();
|
|
399
445
|
const urlMatch = error.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
400
446
|
if (urlMatch && !urlFound) {
|
|
401
447
|
urlFound = true;
|
|
402
448
|
clearTimeout(timeout);
|
|
403
|
-
|
|
404
|
-
child
|
|
449
|
+
resolveOnce({
|
|
450
|
+
child,
|
|
405
451
|
url: urlMatch[0]
|
|
406
452
|
});
|
|
407
453
|
}
|
|
@@ -414,13 +460,13 @@ const unpluginFactory = (options = {}) => {
|
|
|
414
460
|
else for (const line of error.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.error(`[cloudflared stderr] ${line.trim()}`);
|
|
415
461
|
}
|
|
416
462
|
});
|
|
417
|
-
child
|
|
463
|
+
child.on("error", (error) => {
|
|
418
464
|
clearTimeout(timeout);
|
|
419
|
-
|
|
465
|
+
rejectOnce(/* @__PURE__ */ new Error(`Failed to start quick tunnel process: ${error.message}`));
|
|
420
466
|
});
|
|
421
|
-
child
|
|
467
|
+
child.on("exit", (code, signal) => {
|
|
422
468
|
clearTimeout(timeout);
|
|
423
|
-
if (!urlFound)
|
|
469
|
+
if (!urlFound) rejectOnce(/* @__PURE__ */ new Error(`Quick tunnel process exited before URL was found (code: ${code}, signal: ${signal})`));
|
|
424
470
|
});
|
|
425
471
|
});
|
|
426
472
|
};
|
|
@@ -527,14 +573,15 @@ const unpluginFactory = (options = {}) => {
|
|
|
527
573
|
debugLog("[unplugin-cloudflare-tunnel] Starting quick tunnel mode...");
|
|
528
574
|
debugLog("Quick tunnel mode - no API token or hostname required");
|
|
529
575
|
await ensureCloudflaredBinary(bin);
|
|
530
|
-
const localTarget
|
|
531
|
-
debugLog("← Quick tunnel connecting to local target", localTarget
|
|
576
|
+
const localTarget = getLocalTarget(serverHost, port);
|
|
577
|
+
debugLog("← Quick tunnel connecting to local target", localTarget);
|
|
532
578
|
try {
|
|
533
|
-
const { child: quickChild, url } = await spawnQuickTunnel(localTarget
|
|
579
|
+
const { child: quickChild, url } = await spawnQuickTunnel(localTarget, protocol);
|
|
534
580
|
tunnelUrl = url;
|
|
535
581
|
child = quickChild;
|
|
536
582
|
globalState.child = child;
|
|
537
583
|
globalState.configHash = newConfigHash;
|
|
584
|
+
globalState.tunnelUrl = Promise.resolve(url);
|
|
538
585
|
registerExitHandler();
|
|
539
586
|
registerListeningHandler(() => {
|
|
540
587
|
const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
|
|
@@ -554,10 +601,11 @@ const unpluginFactory = (options = {}) => {
|
|
|
554
601
|
killCloudflared("SIGTERM");
|
|
555
602
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
556
603
|
const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
|
|
557
|
-
const { child: newChild, url: newUrl } = await spawnQuickTunnel(newLocalTarget);
|
|
604
|
+
const { child: newChild, url: newUrl } = await spawnQuickTunnel(newLocalTarget, protocol);
|
|
558
605
|
tunnelUrl = newUrl;
|
|
559
606
|
child = newChild;
|
|
560
607
|
globalState.child = child;
|
|
608
|
+
globalState.tunnelUrl = Promise.resolve(newUrl);
|
|
561
609
|
announceTunnel({
|
|
562
610
|
key: `quick:${newUrl}:${actualPort ?? port}`,
|
|
563
611
|
url: newUrl,
|
|
@@ -586,17 +634,15 @@ const unpluginFactory = (options = {}) => {
|
|
|
586
634
|
}
|
|
587
635
|
}
|
|
588
636
|
debugLog("[unplugin-cloudflare-tunnel] Starting named tunnel mode...");
|
|
589
|
-
const apiToken = providedApiToken || process.env.
|
|
590
|
-
if (!apiToken) throw new Error("[unplugin-cloudflare-tunnel] API token is required. Provide it via 'apiToken' option or set the
|
|
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");
|
|
591
639
|
debugLog(`[unplugin-cloudflare-tunnel] Using port ${port}${userProvidedPort === port ? " (user-provided)" : " (from bundler config)"}`);
|
|
592
640
|
await ensureCloudflaredBinary(bin);
|
|
593
|
-
const accounts = await cf(apiToken, "GET", "/accounts", void 0, array(AccountSchema));
|
|
594
|
-
const accountId = forcedAccount || accounts[0]?.id;
|
|
595
|
-
if (!accountId) throw new Error("Unable to determine Cloudflare account ID");
|
|
596
641
|
const apexDomain = hostname.split(".").slice(-2).join(".");
|
|
597
642
|
const parentDomain = hostname.split(".").slice(1).join(".");
|
|
598
643
|
debugLog("← Apex domain", apexDomain);
|
|
599
644
|
debugLog("← Parent domain", parentDomain);
|
|
645
|
+
let resolvedZone;
|
|
600
646
|
let zoneId = forcedZone;
|
|
601
647
|
if (!zoneId) {
|
|
602
648
|
let zones = [];
|
|
@@ -606,8 +652,12 @@ const unpluginFactory = (options = {}) => {
|
|
|
606
652
|
debugLog("← Error fetching zone for parent domain", error);
|
|
607
653
|
}
|
|
608
654
|
if (zones.length === 0) zones = await cf(apiToken, "GET", `/zones?name=${apexDomain}`, void 0, array(ZoneSchema));
|
|
609
|
-
|
|
655
|
+
resolvedZone = zones[0];
|
|
656
|
+
zoneId = resolvedZone?.id;
|
|
610
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");
|
|
611
661
|
if (!zoneId) throw new Error(`Zone ${apexDomain} not found in account ${accountId}`);
|
|
612
662
|
const { autoCleanup = true } = cleanupConfig;
|
|
613
663
|
let tunnel = (await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel?name=${tunnelName}`, void 0, array(TunnelSchema)))[0];
|
|
@@ -731,26 +781,17 @@ const unpluginFactory = (options = {}) => {
|
|
|
731
781
|
console.error(`[unplugin-cloudflare-tunnel] ⚠️ SSL management error: ${sslError.message}`);
|
|
732
782
|
throw sslError;
|
|
733
783
|
}
|
|
734
|
-
const cloudflaredArgs = ["tunnel"];
|
|
735
|
-
cloudflaredArgs.push("--loglevel", effectiveLogLevel);
|
|
736
|
-
if (logFile) cloudflaredArgs.push("--logfile", logFile);
|
|
737
|
-
debugLog("Spawning cloudflared", bin, cloudflaredArgs);
|
|
738
|
-
cloudflaredArgs.push("run", "--token", token);
|
|
739
|
-
child = NodeChildProcess.spawn(bin, cloudflaredArgs, {
|
|
740
|
-
stdio: [
|
|
741
|
-
"ignore",
|
|
742
|
-
"pipe",
|
|
743
|
-
"pipe"
|
|
744
|
-
],
|
|
745
|
-
detached: false,
|
|
746
|
-
windowsHide: true,
|
|
747
|
-
shell: process.platform === "win32"
|
|
748
|
-
});
|
|
749
|
-
debugLog(`[unplugin-cloudflare-tunnel] Process spawned with PID: ${child.pid}`);
|
|
750
|
-
globalState.child = child;
|
|
751
|
-
globalState.configHash = newConfigHash;
|
|
752
|
-
registerExitHandler();
|
|
753
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
|
+
};
|
|
754
795
|
const logCloudflaredLines = (kind, text) => {
|
|
755
796
|
if (globalState.shuttingDown && !debug) return;
|
|
756
797
|
const isVerbose = effectiveLogLevel === "debug" || effectiveLogLevel === "info";
|
|
@@ -770,42 +811,70 @@ const unpluginFactory = (options = {}) => {
|
|
|
770
811
|
else console.error(`${prefix} ${line}`);
|
|
771
812
|
}
|
|
772
813
|
};
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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;
|
|
780
843
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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();
|
|
801
874
|
registerListeningHandler(() => {
|
|
802
875
|
const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
key: `named:${hostname}:${actualPort ?? port}`,
|
|
806
|
-
url: `https://${hostname}`,
|
|
807
|
-
localTarget: actualLocalTarget
|
|
808
|
-
});
|
|
876
|
+
localTargetForAnnouncement = getLocalTarget(actualServerHost, actualPort ?? port);
|
|
877
|
+
announceNamedTunnelIfReady();
|
|
809
878
|
});
|
|
810
879
|
server.httpServer?.once("close", () => {
|
|
811
880
|
killCloudflared("SIGTERM");
|
|
@@ -817,6 +886,7 @@ const unpluginFactory = (options = {}) => {
|
|
|
817
886
|
pluginLog.warn(`Port conflict detected - server is using port ${actualPort} instead of ${port}`);
|
|
818
887
|
pluginLog.info("Updating tunnel configuration...");
|
|
819
888
|
const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
|
|
889
|
+
localTargetForAnnouncement = newLocalTarget;
|
|
820
890
|
debugLog("← Updating local target to", newLocalTarget);
|
|
821
891
|
await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
|
|
822
892
|
hostname,
|
|
@@ -830,6 +900,8 @@ const unpluginFactory = (options = {}) => {
|
|
|
830
900
|
dnsOption,
|
|
831
901
|
sslOption
|
|
832
902
|
});
|
|
903
|
+
if (tunnelReady && activeTunnelProtocol) pluginLog.info(`Tunnel remains connected via ${activeTunnelProtocol.toUpperCase()} after port update`);
|
|
904
|
+
announceNamedTunnelIfReady();
|
|
833
905
|
}
|
|
834
906
|
} catch (error) {
|
|
835
907
|
console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to update tunnel for port change: ${error.message}`);
|
|
@@ -876,6 +948,18 @@ const unpluginFactory = (options = {}) => {
|
|
|
876
948
|
}
|
|
877
949
|
if (modified) debugLog(`[unplugin-cloudflare-tunnel] Configured ${label} devServer.allowedHosts to include ${hostToAllow}`);
|
|
878
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
|
+
};
|
|
879
963
|
const setupWebpackLikeDevServerIntegration = (compiler, bundler) => {
|
|
880
964
|
if ((compiler?.options?.mode ?? process.env.NODE_ENV) === "production") return;
|
|
881
965
|
const optionsContainer = compiler.options;
|
|
@@ -957,19 +1041,8 @@ const unpluginFactory = (options = {}) => {
|
|
|
957
1041
|
config: (config) => {
|
|
958
1042
|
announceConnecting();
|
|
959
1043
|
if (!config.server) config.server = {};
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
if (!config.server.allowedHosts) {
|
|
965
|
-
config.server.allowedHosts = [hostname];
|
|
966
|
-
debugLog(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
|
|
967
|
-
} else if (Array.isArray(config.server.allowedHosts)) {
|
|
968
|
-
if (!config.server.allowedHosts.includes(hostname)) {
|
|
969
|
-
config.server.allowedHosts.push(hostname);
|
|
970
|
-
debugLog(`[unplugin-cloudflare-tunnel] Added ${hostname} to allowed hosts`);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
1044
|
+
ensureViteAllowedHosts(config.server);
|
|
1045
|
+
if (!isQuickMode) debugLog(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
|
|
973
1046
|
},
|
|
974
1047
|
configureServer: (server) => {
|
|
975
1048
|
const configuredPromise = configureServer(server);
|
|
@@ -1012,7 +1085,5 @@ function getLocalTarget(host, port) {
|
|
|
1012
1085
|
return `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
1013
1086
|
}
|
|
1014
1087
|
const CloudflareTunnel = createUnplugin(unpluginFactory);
|
|
1015
|
-
var src_default = CloudflareTunnel;
|
|
1016
|
-
|
|
1017
1088
|
//#endregion
|
|
1018
|
-
export { CloudflareTunnel,
|
|
1089
|
+
export { CloudflareTunnel, CloudflareTunnel as default };
|