vite-plugin-caddy-multiple-tls 1.4.2 → 1.5.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/README.md CHANGED
@@ -20,6 +20,8 @@ export default config;
20
20
  ```
21
21
 
22
22
  Will give this in the terminal, allow you to connect to your app on HTTPS with a self-signed and trusted cert.
23
+
24
+ The plugin defaults `server.host = true` and `server.allowedHosts = true` (plus preview equivalents) so custom hostnames work without extra config. When a domain is resolved, it also defaults `server.hmr` to use `wss` on port `443` and the resolved host, isolating multiple Vite instances without extra config. Override these in your Vite config if you need different values.
23
25
  ```
24
26
  > vite
25
27
 
@@ -52,6 +54,24 @@ const config = defineConfig({
52
54
  export default config;
53
55
  ```
54
56
 
57
+ You can also pass multiple explicit domains:
58
+
59
+ ```js
60
+ // vite.config.js
61
+ import { defineConfig } from 'vite';
62
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
63
+
64
+ const config = defineConfig({
65
+ plugins: [
66
+ caddyTls({
67
+ domain: ['app.localhost', 'api.localhost'],
68
+ })
69
+ ]
70
+ });
71
+
72
+ export default config;
73
+ ```
74
+
55
75
  To derive a domain like `<repo>.<branch>.<baseDomain>` automatically from git (repo name first, then branch):
56
76
 
57
77
  ```js
@@ -78,6 +98,24 @@ For a zero-config experience, use `baseDomain: 'localhost'` (the default) so the
78
98
 
79
99
  For non-`.localhost` domains (like `local.example.test`), keep `internalTls: true` to force Caddy to use its internal CA for certificates.
80
100
 
101
+ If your Caddy Admin API is not on the default `http://localhost:2019`, set `caddyApiUrl`. Empty or whitespace values fall back to the default.
102
+
103
+ ```js
104
+ // vite.config.js
105
+ import { defineConfig } from 'vite';
106
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
107
+
108
+ const config = defineConfig({
109
+ plugins: [
110
+ caddyTls({
111
+ caddyApiUrl: 'http://localhost:2020',
112
+ })
113
+ ]
114
+ });
115
+
116
+ export default config;
117
+ ```
118
+
81
119
  > [!IMPORTANT]
82
120
  > **Hosts file limitation:** If you use a custom domain, you must **manually** add each generated subdomain to your `/etc/hosts` file (e.g., `127.0.0.1 repo.branch.local.example.test`). System hosts files **do not support wildcards** (e.g., `*.local.example.test`), so you lose the benefit of automatic domain resolution that `localhost` provides.
83
121
 
@@ -127,6 +165,8 @@ api.app.localhost
127
165
  ## Development
128
166
  This repo uses npm workspaces. Install from the root with `npm install`, then run workspace scripts like `npm run build --workspace packages/plugin` or `npm run dev --workspace playground`.
129
167
 
168
+ The published package README is synced from the root `README.md` via `packages/plugin/scripts/sync-readme.sh`.
169
+
130
170
  ## Contributing
131
171
  See [CONTRIBUTING.md](./CONTRIBUTING.md) to see how to get started.
132
172
 
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { PluginOption } from 'vite';
2
2
 
3
3
  interface ViteCaddyTlsPluginOptions {
4
4
  /** Explicit domain to proxy without repo/branch derivation */
5
- domain?: string;
5
+ domain?: string | string[];
6
6
  /** Base domain to build <repo>.<branch>.<baseDomain> (defaults to localhost) */
7
7
  baseDomain?: string;
8
8
  /** Optional loopback domain to avoid /etc/hosts edits */
@@ -14,6 +14,8 @@ interface ViteCaddyTlsPluginOptions {
14
14
  cors?: string;
15
15
  /** Override the default Caddy server name (srv0) */
16
16
  serverName?: string;
17
+ /** Override the Caddy Admin API base URL (default: http://localhost:2019) */
18
+ caddyApiUrl?: string;
17
19
  /** Use Caddy's internal CA for the provided domains (defaults to true when baseDomain or domain is set) */
18
20
  internalTls?: boolean;
19
21
  /**
@@ -35,6 +37,6 @@ type LoopbackDomain = 'localtest.me' | 'lvh.me' | 'nip.io';
35
37
  * ```
36
38
  * @returns {Plugin} - a Vite plugin
37
39
  */
38
- declare function viteCaddyTlsPlugin({ domain, baseDomain, loopbackDomain, repo, branch, cors, serverName, internalTls, upstreamHostHeader, }?: ViteCaddyTlsPluginOptions): PluginOption;
40
+ declare function viteCaddyTlsPlugin({ domain, baseDomain, loopbackDomain, repo, branch, cors, serverName, caddyApiUrl, internalTls, upstreamHostHeader, }?: ViteCaddyTlsPluginOptions): PluginOption;
39
41
 
40
42
  export { type ViteCaddyTlsPluginOptions, viteCaddyTlsPlugin as default };
package/dist/index.js CHANGED
@@ -4,8 +4,15 @@ import path from "path";
4
4
 
5
5
  // src/utils.ts
6
6
  import { execSync } from "child_process";
7
- var CADDY_API = "http://localhost:2019";
8
7
  var DEFAULT_SERVER_NAME = "srv0";
8
+ var DEFAULT_CADDY_API_URL = "http://localhost:2019";
9
+ var caddyApiUrl = DEFAULT_CADDY_API_URL;
10
+ function setCaddyApiUrl(url) {
11
+ caddyApiUrl = url;
12
+ }
13
+ function getCaddyApiUrl() {
14
+ return caddyApiUrl;
15
+ }
9
16
  function isRecord(value) {
10
17
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
18
  }
@@ -31,7 +38,7 @@ function validateCaddyIsInstalled() {
31
38
  }
32
39
  async function isCaddyRunning() {
33
40
  try {
34
- const res = await fetch(`${CADDY_API}/config/`);
41
+ const res = await fetch(`${caddyApiUrl}/config/`);
35
42
  return res.ok;
36
43
  } catch (e) {
37
44
  return false;
@@ -51,7 +58,7 @@ async function startCaddy() {
51
58
  }
52
59
  }
53
60
  async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
54
- const serverUrl = `${CADDY_API}/config/apps/http/servers/${serverName}`;
61
+ const serverUrl = `${caddyApiUrl}/config/apps/http/servers/${serverName}`;
55
62
  const res = await fetch(serverUrl);
56
63
  if (res.ok) return;
57
64
  const baseConfig = {
@@ -63,7 +70,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
63
70
  [serverName]: baseConfig
64
71
  }
65
72
  };
66
- const configRes = await fetch(`${CADDY_API}/config/`);
73
+ const configRes = await fetch(`${caddyApiUrl}/config/`);
67
74
  if (!configRes.ok) {
68
75
  const text = await configRes.text();
69
76
  throw new Error(`Failed to read Caddy config: ${text}`);
@@ -75,7 +82,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
75
82
  }
76
83
  const isEmptyConfig = configText.trim() === "" || config === null || isRecord(config) && Object.keys(config).length === 0;
77
84
  if (isEmptyConfig) {
78
- const loadRes = await fetch(`${CADDY_API}/load`, {
85
+ const loadRes = await fetch(`${caddyApiUrl}/load`, {
79
86
  method: "POST",
80
87
  headers: { "Content-Type": "application/json" },
81
88
  body: JSON.stringify({
@@ -97,7 +104,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
97
104
  let hasHttp = isRecord(http);
98
105
  let hasServers = isRecord(servers);
99
106
  if (!hasApps) {
100
- const createAppsRes = await fetch(`${CADDY_API}/config/apps`, {
107
+ const createAppsRes = await fetch(`${caddyApiUrl}/config/apps`, {
101
108
  method: "PUT",
102
109
  headers: { "Content-Type": "application/json" },
103
110
  body: JSON.stringify({})
@@ -109,7 +116,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
109
116
  hasApps = true;
110
117
  }
111
118
  if (!hasHttp) {
112
- const createHttpRes = await fetch(`${CADDY_API}/config/apps/http`, {
119
+ const createHttpRes = await fetch(`${caddyApiUrl}/config/apps/http`, {
113
120
  method: "PUT",
114
121
  headers: { "Content-Type": "application/json" },
115
122
  body: JSON.stringify({ servers: {} })
@@ -122,7 +129,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
122
129
  hasServers = true;
123
130
  }
124
131
  if (!hasServers) {
125
- const createServersRes = await fetch(`${CADDY_API}/config/apps/http/servers`, {
132
+ const createServersRes = await fetch(`${caddyApiUrl}/config/apps/http/servers`, {
126
133
  method: "PUT",
127
134
  headers: { "Content-Type": "application/json" },
128
135
  body: JSON.stringify({})
@@ -143,7 +150,7 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
143
150
  }
144
151
  }
145
152
  async function ensureTlsAutomation() {
146
- const policiesUrl = `${CADDY_API}/config/apps/tls/automation/policies`;
153
+ const policiesUrl = `${caddyApiUrl}/config/apps/tls/automation/policies`;
147
154
  const policiesRes = await fetch(policiesUrl);
148
155
  if (policiesRes.ok) return;
149
156
  const policiesText = await policiesRes.text();
@@ -152,7 +159,7 @@ async function ensureTlsAutomation() {
152
159
  `Failed to initialize Caddy TLS automation: ${policiesText}`
153
160
  );
154
161
  }
155
- const automationRes = await fetch(`${CADDY_API}/config/apps/tls/automation`, {
162
+ const automationRes = await fetch(`${caddyApiUrl}/config/apps/tls/automation`, {
156
163
  method: "PUT",
157
164
  headers: { "Content-Type": "application/json" },
158
165
  body: JSON.stringify({ policies: [] })
@@ -164,7 +171,7 @@ async function ensureTlsAutomation() {
164
171
  `Failed to initialize Caddy TLS automation: ${automationText}`
165
172
  );
166
173
  }
167
- const tlsRes = await fetch(`${CADDY_API}/config/apps/tls`, {
174
+ const tlsRes = await fetch(`${caddyApiUrl}/config/apps/tls`, {
168
175
  method: "PUT",
169
176
  headers: { "Content-Type": "application/json" },
170
177
  body: JSON.stringify({ automation: { policies: [] } })
@@ -231,7 +238,7 @@ async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAM
231
238
  terminal: true
232
239
  };
233
240
  const res = await fetch(
234
- `${CADDY_API}/config/apps/http/servers/${serverName}/routes`,
241
+ `${caddyApiUrl}/config/apps/http/servers/${serverName}/routes`,
235
242
  {
236
243
  method: "POST",
237
244
  // Append to routes list
@@ -255,7 +262,7 @@ async function addTlsPolicy(id, domains) {
255
262
  }
256
263
  ]
257
264
  };
258
- const res = await fetch(`${CADDY_API}/config/apps/tls/automation/policies`, {
265
+ const res = await fetch(`${caddyApiUrl}/config/apps/tls/automation/policies`, {
259
266
  method: "POST",
260
267
  headers: { "Content-Type": "application/json" },
261
268
  body: JSON.stringify(policy)
@@ -269,20 +276,24 @@ async function addTlsPolicy(id, domains) {
269
276
  }
270
277
  }
271
278
  async function removeRoute(id) {
272
- const res = await fetch(`${CADDY_API}/id/${id}`, {
279
+ const res = await fetch(`${caddyApiUrl}/id/${id}`, {
273
280
  method: "DELETE"
274
281
  });
275
282
  if (!res.ok && res.status !== 404) {
276
283
  console.error(`Failed to remove route ${id}`);
284
+ return false;
277
285
  }
286
+ return true;
278
287
  }
279
288
  async function removeTlsPolicy(id) {
280
- const res = await fetch(`${CADDY_API}/id/${id}`, {
289
+ const res = await fetch(`${caddyApiUrl}/id/${id}`, {
281
290
  method: "DELETE"
282
291
  });
283
292
  if (!res.ok && res.status !== 404) {
284
293
  console.error(`Failed to remove TLS policy ${id}`);
294
+ return false;
285
295
  }
296
+ return true;
286
297
  }
287
298
 
288
299
  // src/index.ts
@@ -360,11 +371,24 @@ function normalizeDomain(domain) {
360
371
  if (!trimmed) return null;
361
372
  return trimmed;
362
373
  }
363
- function resolveDomain(options) {
374
+ function normalizeDomains(domains) {
375
+ const domainList = Array.isArray(domains) ? domains : [domains];
376
+ const normalized = domainList.map((domain) => normalizeDomain(domain)).filter((domain) => Boolean(domain));
377
+ if (normalized.length === 0) return null;
378
+ return Array.from(new Set(normalized));
379
+ }
380
+ function normalizeCaddyApiUrl(url) {
381
+ const trimmed = url.trim();
382
+ if (!trimmed) return null;
383
+ return trimmed.replace(/\/+$/g, "");
384
+ }
385
+ function resolveDomains(options) {
364
386
  if (options.domain) {
365
- return normalizeDomain(options.domain);
387
+ return normalizeDomains(options.domain);
366
388
  }
367
- return buildDerivedDomain(options);
389
+ const derivedDomain = buildDerivedDomain(options);
390
+ if (!derivedDomain) return null;
391
+ return [derivedDomain];
368
392
  }
369
393
  function viteCaddyTlsPlugin({
370
394
  domain,
@@ -374,9 +398,21 @@ function viteCaddyTlsPlugin({
374
398
  branch,
375
399
  cors,
376
400
  serverName,
401
+ caddyApiUrl: caddyApiUrl2,
377
402
  internalTls,
378
403
  upstreamHostHeader
379
404
  } = {}) {
405
+ if (caddyApiUrl2 !== void 0) {
406
+ const normalizedApiUrl = normalizeCaddyApiUrl(caddyApiUrl2);
407
+ if (normalizedApiUrl) {
408
+ setCaddyApiUrl(normalizedApiUrl);
409
+ } else {
410
+ setCaddyApiUrl(DEFAULT_CADDY_API_URL);
411
+ console.warn(
412
+ `caddyApiUrl is empty after trimming. Falling back to ${DEFAULT_CADDY_API_URL}.`
413
+ );
414
+ }
415
+ }
380
416
  function isPreviewServer(server) {
381
417
  return server.config.isProduction;
382
418
  }
@@ -399,14 +435,14 @@ function viteCaddyTlsPlugin({
399
435
  const { httpServer, config } = server;
400
436
  const previewMode = isPreviewServer(server);
401
437
  const fallbackPort = previewMode ? getPreviewPort(config) ?? 4173 : config.server.port || 5173;
402
- const resolvedDomain = resolveDomain({
438
+ const resolvedDomains = resolveDomains({
403
439
  domain,
404
440
  baseDomain,
405
441
  loopbackDomain,
406
442
  repo,
407
443
  branch
408
444
  });
409
- const domainArray = resolvedDomain ? [resolvedDomain] : [];
445
+ const domainArray = resolvedDomains ?? [];
410
446
  const routeId = `vite-proxy-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
411
447
  const shouldUseInternalTls = internalTls ?? (baseDomain !== void 0 || loopbackDomain !== void 0 || domain !== void 0);
412
448
  const tlsPolicyId = shouldUseInternalTls ? `${routeId}-tls` : null;
@@ -414,10 +450,30 @@ function viteCaddyTlsPlugin({
414
450
  let resolvedPort = null;
415
451
  let resolvedHost = null;
416
452
  let setupStarted = false;
453
+ function buildDomainResolutionMessage() {
454
+ const issues = [];
455
+ if (domain !== void 0 && !normalizeDomains(domain)) {
456
+ issues.push("`domain` is empty after trimming");
457
+ }
458
+ if (baseDomain !== void 0 && !normalizeBaseDomain(baseDomain)) {
459
+ issues.push("`baseDomain` is empty after trimming");
460
+ }
461
+ const info = getGitRepoInfo();
462
+ const resolvedRepo = repo ?? info.repo;
463
+ const resolvedBranch = branch ?? info.branch;
464
+ if (!resolvedRepo) {
465
+ issues.push("repo name not found (not a git repo?)");
466
+ }
467
+ if (!resolvedBranch) {
468
+ issues.push("branch name not found (detached HEAD?)");
469
+ }
470
+ if (issues.length === 0) {
471
+ return "No domain resolved. Provide `domain`, or `repo` and `branch`, or ensure git metadata is available.";
472
+ }
473
+ return `No domain resolved. Issues: ${issues.join("; ")}. Provide \`domain\`, or \`repo\` and \`branch\`, or ensure git metadata is available.`;
474
+ }
417
475
  if (domainArray.length === 0) {
418
- console.error(
419
- "No domain resolved. Provide domain, or run inside a git repo, or pass repo/branch."
420
- );
476
+ console.error(buildDomainResolutionMessage());
421
477
  return;
422
478
  }
423
479
  let tlsPolicyAdded = false;
@@ -487,13 +543,19 @@ function viteCaddyTlsPlugin({
487
543
  updateResolvedTarget();
488
544
  return resolvedHost ?? "127.0.0.1";
489
545
  }
546
+ function formatUpstreamTarget(host, port) {
547
+ if (host.includes(":") && !host.startsWith("[")) {
548
+ return `[${host}]:${port}`;
549
+ }
550
+ return `${host}:${port}`;
551
+ }
490
552
  async function cleanupRoute() {
491
553
  if (cleanupStarted) return;
492
554
  cleanupStarted = true;
493
555
  if (tlsPolicyId) {
494
- await removeTlsPolicy(tlsPolicyId);
556
+ await removeWithRetry(() => removeTlsPolicy(tlsPolicyId), "TLS policy");
495
557
  }
496
- await removeRoute(routeId);
558
+ await removeWithRetry(() => removeRoute(routeId), "route");
497
559
  }
498
560
  function onServerClose() {
499
561
  void cleanupRoute();
@@ -515,6 +577,24 @@ function viteCaddyTlsPlugin({
515
577
  process.once("SIGINT", onSigint);
516
578
  process.once("SIGTERM", onSigterm);
517
579
  }
580
+ function wait(ms) {
581
+ return new Promise((resolve) => setTimeout(resolve, ms));
582
+ }
583
+ async function removeWithRetry(remover, label, maxAttempts = 3) {
584
+ let attempt = 0;
585
+ let delayMs = 100;
586
+ while (attempt < maxAttempts) {
587
+ const ok = await remover();
588
+ if (ok) return true;
589
+ attempt += 1;
590
+ if (attempt < maxAttempts) {
591
+ await wait(delayMs);
592
+ delayMs *= 2;
593
+ }
594
+ }
595
+ console.error(`Failed to remove ${label} after ${maxAttempts} attempts.`);
596
+ return false;
597
+ }
518
598
  async function setupRoute() {
519
599
  if (!validateCaddyIsInstalled()) {
520
600
  return;
@@ -530,7 +610,10 @@ function viteCaddyTlsPlugin({
530
610
  try {
531
611
  await ensureBaseConfig(serverName);
532
612
  } catch (e) {
533
- console.error("Failed to configure Caddy base settings.", e);
613
+ console.error(
614
+ `Failed to configure Caddy base settings. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
615
+ e
616
+ );
534
617
  return;
535
618
  }
536
619
  const port = getServerPort();
@@ -540,7 +623,10 @@ function viteCaddyTlsPlugin({
540
623
  await addTlsPolicy(tlsPolicyId, domainArray);
541
624
  tlsPolicyAdded = true;
542
625
  } catch (e) {
543
- console.error("Failed to add TLS policy to Caddy.", e);
626
+ console.error(
627
+ `Failed to add TLS policy to Caddy. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
628
+ e
629
+ );
544
630
  return;
545
631
  }
546
632
  }
@@ -558,10 +644,15 @@ function viteCaddyTlsPlugin({
558
644
  if (tlsPolicyAdded && tlsPolicyId) {
559
645
  await removeTlsPolicy(tlsPolicyId);
560
646
  }
561
- console.error("Failed to add route to Caddy.", e);
647
+ console.error(
648
+ `Failed to add route to Caddy. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
649
+ e
650
+ );
562
651
  return;
563
652
  }
564
653
  console.log("\n\u{1F512} Caddy is proxying your traffic on https");
654
+ console.log(`
655
+ \u27A1\uFE0F Upstream target: http://${formatUpstreamTarget(upstreamHost, port)}`);
565
656
  console.log(
566
657
  `
567
658
  \u{1F517} Access your local ${domainArray.length > 1 ? "servers" : "server"}!`
@@ -615,10 +706,24 @@ function viteCaddyTlsPlugin({
615
706
  return {
616
707
  name: "vite:caddy-tls",
617
708
  config(userConfig) {
709
+ const resolvedDomains = resolveDomains({
710
+ domain,
711
+ baseDomain,
712
+ loopbackDomain,
713
+ repo,
714
+ branch
715
+ });
716
+ const defaultHmrDomain = resolvedDomains?.[0];
717
+ const hmrConfig = userConfig.server?.hmr === void 0 && defaultHmrDomain ? {
718
+ protocol: "wss",
719
+ host: defaultHmrDomain,
720
+ clientPort: 443
721
+ } : userConfig.server?.hmr;
618
722
  return {
619
723
  server: {
620
724
  host: userConfig.server?.host === void 0 ? true : userConfig.server.host,
621
- allowedHosts: userConfig.server?.allowedHosts === void 0 ? true : userConfig.server.allowedHosts
725
+ allowedHosts: userConfig.server?.allowedHosts === void 0 ? true : userConfig.server.allowedHosts,
726
+ ...hmrConfig !== void 0 ? { hmr: hmrConfig } : {}
622
727
  },
623
728
  preview: {
624
729
  host: userConfig.preview?.host === void 0 ? true : userConfig.preview.host,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-caddy-multiple-tls",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Vite plugin that uses Caddy to provide local HTTPS with derived domains.",
5
5
  "keywords": [
6
6
  "vite",