vite-plugin-caddy-multiple-tls 1.0.0 → 1.2.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
@@ -1,28 +1,135 @@
1
1
  # vite-plugin-caddy-multiple-tls
2
2
 
3
- Vite plugin that runs Caddy to proxy local development traffic over HTTPS with
4
- derived domains like `<repo>.<branch>.localhost`.
3
+ ## What it does
4
+ Runs Caddy alongside Vite to give you HTTPS locally with automatic, per-branch domains like `<repo>.<branch>.localhost`, so you can use real hostnames, cookies, and secure APIs without manual proxy setup.
5
5
 
6
- ## Install
6
+ ## Usage
7
+
8
+ ```js
9
+ // vite.config.js
10
+ import { defineConfig } from 'vite';
11
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
7
12
 
8
- ```sh
9
- npm install -D vite-plugin-caddy-multiple-tls
13
+ const config = defineConfig({
14
+ plugins: [
15
+ caddyTls(),
16
+ ]
17
+ });
18
+
19
+ export default config;
10
20
  ```
11
21
 
12
- ## Usage
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
+ > vite
25
+
26
+
27
+ 🔒 Caddy is proxying your traffic on https
28
+
29
+ 🔗 Access your local server
30
+ 🌍 https://my-repo.my-branch.localhost
31
+
32
+ ```
33
+
34
+ By default, the plugin derives `<repo>.<branch>.localhost` from git.
35
+ If repo or branch can't be detected, pass `repo`/`branch` or use `domain`.
36
+
37
+ If you want a fixed host without repo/branch in the URL, pass a single domain:
13
38
 
14
39
  ```js
40
+ // vite.config.js
15
41
  import { defineConfig } from 'vite';
16
42
  import caddyTls from 'vite-plugin-caddy-multiple-tls';
17
43
 
18
- export default defineConfig({
19
- plugins: [caddyTls()],
44
+ const config = defineConfig({
45
+ plugins: [
46
+ caddyTls({
47
+ domain: 'app.localhost',
48
+ })
49
+ ]
20
50
  });
51
+
52
+ export default config;
21
53
  ```
22
54
 
23
- ## Options
55
+ To derive a domain like `<repo>.<branch>.<baseDomain>` automatically from git (repo name first, then branch):
56
+
57
+ ```js
58
+ // vite.config.js
59
+ import { defineConfig } from 'vite';
60
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
61
+
62
+ const config = defineConfig({
63
+ plugins: [
64
+ caddyTls({
65
+ baseDomain: 'local.conekto.eu',
66
+ })
67
+ ]
68
+ });
69
+
70
+ export default config;
71
+ ```
72
+
73
+ You can override auto-detection with `repo` or `branch` if needed.
74
+
75
+ For a zero-config experience, use `baseDomain: 'localhost'` (the default) so the derived domain works without editing `/etc/hosts`.
76
+
77
+ `internalTls` defaults to `true` when you pass `baseDomain` or `domain`. You can override it if needed.
78
+
79
+ For non-`.localhost` domains (like `local.example.test`), keep `internalTls: true` to force Caddy to use its internal CA for certificates.
80
+
81
+ > [!IMPORTANT]
82
+ > **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
+
84
+ ## Recommended base domain: `.localhost`
85
+ Why `localhost` is the best option for local development:
86
+ - Reserved by RFC 6761 (never on the public internet).
87
+ - Automatic resolution on macOS: `*.localhost` maps to `127.0.0.1` and `::1` without DNS or `/etc/hosts`.
88
+ - Subdomain support: `api.localhost`, `foo.bar.localhost`, etc.
89
+ - Secure context in browsers for HTTPS, service workers, and cookies.
90
+ - Works well with Caddy and other local reverse proxies.
91
+
92
+ Example usage:
93
+ ```
94
+ app.localhost
95
+ api.app.localhost
96
+ ```
97
+
98
+ > [!NOTE]
99
+ > **Linux users:** Unlike macOS, most Linux distributions don't automatically resolve `*.localhost` subdomains. The plugin will detect Linux and show you the exact command to run:
100
+ > ```
101
+ > 🐧 Linux users: if the domain doesn't resolve, run:
102
+ > echo "127.0.0.1 my-repo.my-branch.localhost" | sudo tee -a /etc/hosts
103
+ > ```
104
+ >
105
+ > If you want to avoid `/etc/hosts` edits on Linux, set `loopbackDomain` to a public loopback domain:
106
+ > ```ts
107
+ > caddyTls({
108
+ > loopbackDomain: 'localtest.me',
109
+ > })
110
+ > ```
111
+ > Supported values: `localtest.me`, `lvh.me`, `nip.io` (maps to `127.0.0.1.nip.io`). These rely on public DNS, so they can fail offline or on restricted networks.
112
+ >
113
+ > Why these work: they use wildcard DNS so any subdomain resolves to `127.0.0.1`, meaning the request loops back to your machine after DNS.
114
+ > - `localtest.me` and `lvh.me`: static wildcard -> always `127.0.0.1` (great for subdomain testing).
115
+ > - `nip.io`: dynamic parsing of the IP in the hostname (e.g. `app.192.168.1.50.nip.io`) so you can target LAN devices.
116
+ > Why use them: subdomains behave like real domains, no `/etc/hosts` edits, and closer parity for cookies/CORS rules.
117
+ >
118
+ > When using loopback domains, ensure your Vite config allows the Host header, e.g. `server: { allowedHosts: true }`.
119
+ >
120
+ > For a permanent fix that handles all `*.localhost` domains automatically, install dnsmasq:
121
+ > ```bash
122
+ > sudo apt install dnsmasq
123
+ > echo "address=/.localhost/127.0.0.1" | sudo tee /etc/dnsmasq.d/localhost.conf
124
+ > sudo systemctl restart dnsmasq
125
+ > ```
126
+
127
+ ## Development
128
+ 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
+
130
+ ## Contributing
131
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) to see how to get started.
132
+
133
+ ## License
24
134
 
25
- - `domain`: explicit domain to proxy without repo/branch derivation
26
- - `baseDomain`: base domain to build `<repo>.<branch>.<baseDomain>` (defaults to `localhost`)
27
- - `repo`, `branch`: override repo/branch names used for derived domains
28
- - `internalTls`: use Caddy internal CA for provided domains
135
+ MIT
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ interface ViteCaddyTlsPluginOptions {
5
5
  domain?: string;
6
6
  /** Base domain to build <repo>.<branch>.<baseDomain> (defaults to localhost) */
7
7
  baseDomain?: string;
8
+ /** Optional loopback domain to avoid /etc/hosts edits */
9
+ loopbackDomain?: LoopbackDomain;
8
10
  /** Override repo name used in derived domains */
9
11
  repo?: string;
10
12
  /** Override branch name used in derived domains */
@@ -15,6 +17,7 @@ interface ViteCaddyTlsPluginOptions {
15
17
  /** Use Caddy's internal CA for the provided domains (defaults to true when baseDomain or domain is set) */
16
18
  internalTls?: boolean;
17
19
  }
20
+ type LoopbackDomain = 'localtest.me' | 'lvh.me' | 'nip.io';
18
21
  /**
19
22
  * Vite plugin to run Caddy server to proxy traffic on https for local development
20
23
  *
@@ -27,6 +30,6 @@ interface ViteCaddyTlsPluginOptions {
27
30
  * ```
28
31
  * @returns {Plugin} - a Vite plugin
29
32
  */
30
- declare function viteCaddyTlsPlugin({ domain, baseDomain, repo, branch, cors, serverName, internalTls, }?: ViteCaddyTlsPluginOptions): PluginOption;
33
+ declare function viteCaddyTlsPlugin({ domain, baseDomain, loopbackDomain, repo, branch, cors, serverName, internalTls, }?: ViteCaddyTlsPluginOptions): PluginOption;
31
34
 
32
35
  export { type ViteCaddyTlsPluginOptions, viteCaddyTlsPlugin as default };
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/index.ts
2
- import chalk from "chalk";
3
2
  import { execSync as execSync2 } from "child_process";
4
3
  import path from "path";
5
4
 
@@ -175,7 +174,13 @@ async function ensureTlsAutomation() {
175
174
  throw new Error(`Failed to initialize Caddy TLS automation: ${text}`);
176
175
  }
177
176
  }
178
- async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAME) {
177
+ function formatDialAddress(host, port) {
178
+ if (host.includes(":") && !host.startsWith("[")) {
179
+ return `[${host}]:${port}`;
180
+ }
181
+ return `${host}:${port}`;
182
+ }
183
+ async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAME, upstreamHost = "127.0.0.1") {
179
184
  const handlers = [];
180
185
  if (cors) {
181
186
  handlers.push({
@@ -198,7 +203,7 @@ async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAM
198
203
  }
199
204
  handlers.push({
200
205
  handler: "reverse_proxy",
201
- upstreams: [{ dial: `localhost:${port}` }]
206
+ upstreams: [{ dial: formatDialAddress(upstreamHost, port) }]
202
207
  });
203
208
  const route = {
204
209
  "@id": id,
@@ -271,6 +276,11 @@ async function removeTlsPolicy(id) {
271
276
  }
272
277
 
273
278
  // src/index.ts
279
+ var LOOPBACK_DOMAINS = {
280
+ "localtest.me": "localtest.me",
281
+ "lvh.me": "lvh.me",
282
+ "nip.io": "127.0.0.1.nip.io"
283
+ };
274
284
  function execGit(command) {
275
285
  return execSync2(command, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
276
286
  }
@@ -298,11 +308,29 @@ function getGitRepoInfo() {
298
308
  function normalizeBaseDomain(baseDomain) {
299
309
  return baseDomain.trim().replace(/^\.+|\.+$/g, "").toLowerCase();
300
310
  }
311
+ function resolveBaseDomain(options) {
312
+ if (options.baseDomain !== void 0) {
313
+ return normalizeBaseDomain(options.baseDomain);
314
+ }
315
+ if (options.loopbackDomain) {
316
+ return normalizeBaseDomain(LOOPBACK_DOMAINS[options.loopbackDomain]);
317
+ }
318
+ return "localhost";
319
+ }
320
+ function resolveUpstreamHost(host) {
321
+ if (typeof host === "string") {
322
+ const trimmed = host.trim();
323
+ if (trimmed && trimmed !== "0.0.0.0" && trimmed !== "::") {
324
+ return trimmed;
325
+ }
326
+ }
327
+ return "127.0.0.1";
328
+ }
301
329
  function sanitizeDomainLabel(value) {
302
330
  return value.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
303
331
  }
304
332
  function buildDerivedDomain(options) {
305
- const baseDomain = normalizeBaseDomain(options.baseDomain ?? "localhost");
333
+ const baseDomain = resolveBaseDomain(options);
306
334
  if (!baseDomain) return null;
307
335
  let repo = options.repo;
308
336
  let branch = options.branch;
@@ -331,6 +359,7 @@ function resolveDomain(options) {
331
359
  function viteCaddyTlsPlugin({
332
360
  domain,
333
361
  baseDomain,
362
+ loopbackDomain,
334
363
  repo,
335
364
  branch,
336
365
  cors,
@@ -339,30 +368,85 @@ function viteCaddyTlsPlugin({
339
368
  } = {}) {
340
369
  return {
341
370
  name: "vite:caddy-tls",
342
- configureServer({ httpServer, config }) {
371
+ configureServer(server) {
372
+ const { httpServer, config } = server;
343
373
  const fallbackPort = config.server.port || 5173;
344
- const resolvedDomain = resolveDomain({ domain, baseDomain, repo, branch });
374
+ const resolvedDomain = resolveDomain({
375
+ domain,
376
+ baseDomain,
377
+ loopbackDomain,
378
+ repo,
379
+ branch
380
+ });
345
381
  const domainArray = resolvedDomain ? [resolvedDomain] : [];
346
382
  const routeId = `vite-proxy-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
347
- const shouldUseInternalTls = internalTls ?? (baseDomain !== void 0 || domain !== void 0);
383
+ const shouldUseInternalTls = internalTls ?? (baseDomain !== void 0 || loopbackDomain !== void 0 || domain !== void 0);
348
384
  const tlsPolicyId = shouldUseInternalTls ? `${routeId}-tls` : null;
349
385
  let cleanupStarted = false;
386
+ let resolvedPort = null;
387
+ let resolvedHost = null;
388
+ let setupStarted = false;
350
389
  if (domainArray.length === 0) {
351
390
  console.error(
352
- chalk.red(
353
- "No domain resolved. Provide domain, or run inside a git repo, or pass repo/branch."
354
- )
391
+ "No domain resolved. Provide domain, or run inside a git repo, or pass repo/branch."
355
392
  );
356
393
  return;
357
394
  }
358
395
  let tlsPolicyAdded = false;
359
- function getServerPort() {
360
- if (!httpServer) return fallbackPort;
361
- const address = httpServer.address();
396
+ function getPortFromAddress(address) {
362
397
  if (address && typeof address === "object" && "port" in address) {
363
- return address.port;
398
+ const port = address.port;
399
+ if (typeof port === "number") {
400
+ return port;
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+ function updateResolvedTarget() {
406
+ if (resolvedPort !== null && resolvedHost !== null) return;
407
+ const resolvedUrl = server.resolvedUrls?.local?.[0];
408
+ if (resolvedUrl) {
409
+ try {
410
+ const url = new URL(resolvedUrl);
411
+ if (resolvedHost === null && url.hostname) {
412
+ resolvedHost = url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
413
+ }
414
+ const port = Number(url.port);
415
+ if (resolvedPort === null && !Number.isNaN(port)) {
416
+ resolvedPort = port;
417
+ }
418
+ } catch (e) {
419
+ }
420
+ }
421
+ if (httpServer) {
422
+ const address = httpServer.address();
423
+ if (address && typeof address === "object") {
424
+ const port = getPortFromAddress(address);
425
+ if (resolvedPort === null && port !== null) {
426
+ resolvedPort = port;
427
+ }
428
+ if (resolvedHost === null && "address" in address) {
429
+ const host = address.address;
430
+ if (typeof host === "string" && host !== "0.0.0.0" && host !== "::") {
431
+ resolvedHost = host;
432
+ }
433
+ }
434
+ }
435
+ }
436
+ if (resolvedPort === null && typeof config.server.port === "number") {
437
+ resolvedPort = config.server.port;
364
438
  }
365
- return fallbackPort;
439
+ if (resolvedHost === null) {
440
+ resolvedHost = resolveUpstreamHost(config.server.host);
441
+ }
442
+ }
443
+ function getServerPort() {
444
+ updateResolvedTarget();
445
+ return resolvedPort ?? fallbackPort;
446
+ }
447
+ function getUpstreamHost() {
448
+ updateResolvedTarget();
449
+ return resolvedHost ?? "127.0.0.1";
366
450
  }
367
451
  async function cleanupRoute() {
368
452
  if (cleanupStarted) return;
@@ -400,64 +484,87 @@ function viteCaddyTlsPlugin({
400
484
  if (!running) {
401
485
  running = await startCaddy();
402
486
  if (!running) {
403
- console.error(chalk.red("Failed to start Caddy server."));
487
+ console.error("Failed to start Caddy server.");
404
488
  return;
405
489
  }
406
490
  }
407
491
  try {
408
492
  await ensureBaseConfig(serverName);
409
493
  } catch (e) {
410
- console.error(chalk.red("Failed to configure Caddy base settings."), e);
494
+ console.error("Failed to configure Caddy base settings.", e);
411
495
  return;
412
496
  }
413
497
  const port = getServerPort();
498
+ const upstreamHost = getUpstreamHost();
414
499
  if (tlsPolicyId) {
415
500
  try {
416
501
  await addTlsPolicy(tlsPolicyId, domainArray);
417
502
  tlsPolicyAdded = true;
418
503
  } catch (e) {
419
- console.error(chalk.red("Failed to add TLS policy to Caddy."), e);
504
+ console.error("Failed to add TLS policy to Caddy.", e);
420
505
  return;
421
506
  }
422
507
  }
423
508
  try {
424
- await addRoute(routeId, domainArray, port, cors, serverName);
509
+ await addRoute(routeId, domainArray, port, cors, serverName, upstreamHost);
425
510
  } catch (e) {
426
511
  if (tlsPolicyAdded && tlsPolicyId) {
427
512
  await removeTlsPolicy(tlsPolicyId);
428
513
  }
429
- console.error(chalk.red("Failed to add route to Caddy."), e);
514
+ console.error("Failed to add route to Caddy.", e);
430
515
  return;
431
516
  }
432
517
  console.log();
433
- console.log(chalk.green("\u{1F512} Caddy is proxying your traffic on https"));
518
+ console.log("\u{1F512} Caddy is proxying your traffic on https");
434
519
  console.log();
435
520
  console.log(
436
- `\u{1F517} Access your local ${domainArray.length > 1 ? "servers" : "server"} `
521
+ `\u{1F517} Access your local ${domainArray.length > 1 ? "servers" : "server"}!`
437
522
  );
438
523
  domainArray.forEach((domain2) => {
439
- console.log(chalk.blue(`\u{1F30D} https://${domain2}`));
524
+ console.log(`\u{1F30D} https://${domain2}`);
440
525
  });
441
- if (process.platform === "linux") {
526
+ if (process.platform === "linux" && !loopbackDomain) {
442
527
  console.log();
443
- console.log(chalk.yellow("\u{1F427} Linux users: if the domain doesn't resolve, run:"));
528
+ console.log("\u{1F427} Linux users: if the domain doesn't resolve, run:");
444
529
  domainArray.forEach((domain2) => {
445
- console.log(chalk.dim(` echo "127.0.0.1 ${domain2}" | sudo tee -a /etc/hosts`));
530
+ console.log(` echo "127.0.0.1 ${domain2}" | sudo tee -a /etc/hosts`);
446
531
  });
447
532
  }
448
533
  console.log();
449
534
  registerProcessCleanup();
450
535
  httpServer?.once("close", onServerClose);
451
536
  }
452
- function onListening() {
537
+ function runSetupOnce() {
538
+ if (setupStarted) return;
539
+ setupStarted = true;
453
540
  void setupRoute();
454
541
  }
542
+ function wrapServerListen() {
543
+ if (typeof server.listen !== "function") return false;
544
+ const originalListen = server.listen.bind(server);
545
+ server.listen = async function(port, isRestart) {
546
+ const result = await originalListen(port, isRestart);
547
+ if (typeof port === "number") {
548
+ resolvedPort = port;
549
+ } else {
550
+ updateResolvedTarget();
551
+ }
552
+ runSetupOnce();
553
+ return result;
554
+ };
555
+ return true;
556
+ }
557
+ function onListening() {
558
+ updateResolvedTarget();
559
+ runSetupOnce();
560
+ }
561
+ const listenWrapped = wrapServerListen();
455
562
  if (httpServer?.listening) {
456
- void setupRoute();
563
+ runSetupOnce();
457
564
  } else if (httpServer) {
458
565
  httpServer.once("listening", onListening);
459
- } else {
460
- void setupRoute();
566
+ } else if (!listenWrapped) {
567
+ runSetupOnce();
461
568
  }
462
569
  }
463
570
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-caddy-multiple-tls",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Vite plugin that uses Caddy to provide local HTTPS with derived domains.",
5
5
  "keywords": [
6
6
  "vite",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "scripts": {
31
31
  "dev": "tsup --watch src/**/* src/index.ts --format esm --dts-resolve",
32
- "build": "tsup src/index.ts --format esm --dts",
32
+ "sync-readme": "bash scripts/sync-readme.sh",
33
+ "build": "npm run sync-readme && tsup src/index.ts --format esm --dts",
33
34
  "prepublishOnly": "npm run build",
34
35
  "test": "vitest run",
35
36
  "test:watch": "vitest"
@@ -55,8 +56,5 @@
55
56
  "tsup": "^8.5.1",
56
57
  "vite": "^7.3.0",
57
58
  "vitest": "^4.0.16"
58
- },
59
- "dependencies": {
60
- "chalk": "^5.6.2"
61
59
  }
62
60
  }