@hamedb89/localghost 0.1.3 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.js CHANGED
@@ -1,12 +1,16 @@
1
+ // src/vite.ts
2
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
3
+ import { normalize, resolve as resolve2 } from "path";
4
+
1
5
  // src/config.ts
2
6
  import { existsSync, readFileSync, readdirSync } from "fs";
3
7
  import { basename, join, resolve } from "path";
4
8
 
5
9
  // src/parse.ts
6
10
  var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
7
- function parseDevHosts(input, fileName = ".localghost") {
11
+ function parseDevHosts(input2, fileName = ".localghost") {
8
12
  const entries = [];
9
- input.split(/\r?\n/).forEach((rawLine, index) => {
13
+ input2.split(/\r?\n/).forEach((rawLine, index) => {
10
14
  const line = rawLine.replace(/#.*/, "").trim();
11
15
  if (!line) {
12
16
  return;
@@ -100,6 +104,357 @@ function readDevHosts(options = {}) {
100
104
  }
101
105
  return parseDevHosts(readFileSync(resolvedPath.path, "utf8"), resolvedPath.fileName);
102
106
  }
107
+ function getProjectName(cwd = process.cwd()) {
108
+ try {
109
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
110
+ const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
111
+ return sanitizeProjectName(name.replace(/^@/, ""));
112
+ } catch {
113
+ return "app";
114
+ }
115
+ }
116
+ function sanitizeProjectName(value) {
117
+ const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
118
+ return projectName || "app";
119
+ }
120
+
121
+ // src/context.ts
122
+ import { existsSync as existsSync2 } from "fs";
123
+ import { pathToFileURL } from "url";
124
+
125
+ // src/port.ts
126
+ import { createServer } from "net";
127
+ async function isPortAvailable(port, host = "127.0.0.1") {
128
+ return new Promise((resolve3) => {
129
+ const server = createServer();
130
+ server.once("error", () => {
131
+ resolve3(false);
132
+ });
133
+ server.once("listening", () => {
134
+ server.close(() => resolve3(true));
135
+ });
136
+ server.listen(port, host);
137
+ });
138
+ }
139
+ async function findAvailablePort(startPort, options = {}) {
140
+ const host = options.host ?? "127.0.0.1";
141
+ const maxAttempts = options.maxAttempts ?? 50;
142
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
143
+ const port = startPort + offset;
144
+ if (await isPortAvailable(port, host)) {
145
+ return port;
146
+ }
147
+ }
148
+ throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
149
+ }
150
+
151
+ // src/context.ts
152
+ var LOCALGHOST_PROJECT_CONFIG_FILES = [
153
+ "localghost.config.mjs",
154
+ "localghost.config.js",
155
+ "localghost.config.cjs"
156
+ ];
157
+ function parsePort(value) {
158
+ if (!value) return void 0;
159
+ const port = Number.parseInt(value, 10);
160
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
161
+ }
162
+ function envPort() {
163
+ return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
164
+ }
165
+ function envDynamicPort() {
166
+ const value = process.env.LOCALGHOST_DYNAMIC_PORT;
167
+ if (!value) return void 0;
168
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
169
+ }
170
+ function envHttps() {
171
+ const value = process.env.LOCALGHOST_HTTPS;
172
+ if (!value) return void 0;
173
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
174
+ }
175
+ function readOptionsFromContext(options) {
176
+ return {
177
+ cwd: options.cwd ?? process.cwd(),
178
+ ...options.fileName ? { fileName: options.fileName } : {},
179
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
180
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
181
+ };
182
+ }
183
+ function withRuntimePort(entries, requestedPort, port) {
184
+ if (requestedPort === port) return entries;
185
+ const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
186
+ if (!hasRequestedPort) return entries;
187
+ return entries.map((entry) => entry.port === requestedPort ? { ...entry, port } : entry);
188
+ }
189
+ function uniqueHosts(entries) {
190
+ return [...new Set(entries.map((entry) => entry.host))];
191
+ }
192
+ function isAliasableHost(host) {
193
+ return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
194
+ }
195
+ function getDefaultWwwAlias(host) {
196
+ return isAliasableHost(host) ? `www.${host}` : null;
197
+ }
198
+ function addDefaultWwwAliases(entries) {
199
+ const seen = new Set(entries.map((entry) => entry.host));
200
+ const aliases = [];
201
+ for (const entry of entries) {
202
+ const alias = getDefaultWwwAlias(entry.host);
203
+ if (alias && !seen.has(alias)) {
204
+ aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
205
+ seen.add(alias);
206
+ }
207
+ }
208
+ return [...entries, ...aliases];
209
+ }
210
+ function defined(input2) {
211
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
212
+ }
213
+ async function readProjectConfig(cwd, configFile) {
214
+ if (configFile === false) return {};
215
+ const candidates = configFile ? [configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
216
+ const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync2(candidate));
217
+ if (!path) return {};
218
+ const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
219
+ const config = imported.default ?? imported;
220
+ return { config, path };
221
+ }
222
+ async function resolveLocalghostContext(options = {}) {
223
+ const cwd = options.cwd ?? process.cwd();
224
+ const projectConfig = await readProjectConfig(cwd, options.localghostConfig);
225
+ const merged = {
226
+ ...projectConfig.config,
227
+ ...defined(options)
228
+ };
229
+ const readOptions = readOptionsFromContext({ ...merged, cwd });
230
+ const resolvedPath = resolveDevHostsPath(readOptions);
231
+ const configEntries = readDevHosts(readOptions);
232
+ const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
233
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? false;
234
+ const bindHost = merged.bindHost ?? "127.0.0.1";
235
+ const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
236
+ const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
237
+ const wwwAlias = merged.wwwAlias ?? true;
238
+ const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
239
+ const hosts = uniqueHosts(entries);
240
+ const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
241
+ return {
242
+ cwd,
243
+ projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
244
+ readOptions,
245
+ configPath: resolvedPath.path,
246
+ configFileName: resolvedPath.fileName,
247
+ configEntries,
248
+ entries,
249
+ hosts,
250
+ requestedPort,
251
+ port,
252
+ dynamicPort,
253
+ bindHost,
254
+ primaryHost,
255
+ https: merged.https ?? envHttps() ?? false,
256
+ wwwAlias,
257
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
258
+ };
259
+ }
260
+
261
+ // src/doctor.ts
262
+ import { execa } from "execa";
263
+ async function checkCaddy() {
264
+ try {
265
+ const result = await execa("caddy", ["version"], { reject: false });
266
+ const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
267
+ return {
268
+ found: result.exitCode === 0,
269
+ ...version ? { version } : {},
270
+ installHint: "brew install caddy"
271
+ };
272
+ } catch {
273
+ return {
274
+ found: false,
275
+ installHint: "brew install caddy"
276
+ };
277
+ }
278
+ }
279
+
280
+ // src/env.ts
281
+ function getProductionReason(env = process.env) {
282
+ if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
283
+ if (env.NODE_ENV === "production") return "NODE_ENV=production";
284
+ if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
285
+ if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
286
+ if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
287
+ return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
288
+ }
289
+ return null;
290
+ }
291
+ function isProductionLike(env = process.env) {
292
+ return getProductionReason(env) !== null;
293
+ }
294
+
295
+ // src/fs.ts
296
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
297
+ import { dirname } from "path";
298
+ function readTextFile(path) {
299
+ return readFileSync2(path, "utf8");
300
+ }
301
+ function writeTextFile(path, value) {
302
+ mkdirSync(dirname(path), { recursive: true });
303
+ writeFileSync(path, value, "utf8");
304
+ return path;
305
+ }
306
+
307
+ // src/hosts-file.ts
308
+ import { writeFileSync as writeFileSync2 } from "fs";
309
+ import { tmpdir } from "os";
310
+ import { join as join2 } from "path";
311
+ import { execa as execa2 } from "execa";
312
+ function escapeRegExp(value) {
313
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
314
+ }
315
+ function getManagedBlockPattern(projectName) {
316
+ const sanitizedProjectName = sanitizeProjectName(projectName);
317
+ const start = `# localghost:start ${sanitizedProjectName}`;
318
+ const end = `# localghost:end ${sanitizedProjectName}`;
319
+ return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
320
+ }
321
+ function getSystemHostsPath() {
322
+ return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
323
+ }
324
+ function renderHostsBlock(projectName, entries) {
325
+ const sanitizedProjectName = sanitizeProjectName(projectName);
326
+ const hosts = [...new Set(entries.map((entry) => entry.host))].sort();
327
+ return [
328
+ `# localghost:start ${sanitizedProjectName}`,
329
+ ...hosts.map((host) => `127.0.0.1 ${host}`),
330
+ `# localghost:end ${sanitizedProjectName}`,
331
+ ""
332
+ ].join("\n");
333
+ }
334
+ function upsertManagedBlock(existing, projectName, block) {
335
+ const pattern = getManagedBlockPattern(projectName);
336
+ if (pattern.test(existing)) {
337
+ return existing.replace(pattern, block);
338
+ }
339
+ return `${existing.trimEnd()}
340
+
341
+ ${block}`;
342
+ }
343
+ async function writeSystemHostsFile(hostsPath, next, projectName) {
344
+ const sanitizedProjectName = sanitizeProjectName(projectName);
345
+ const tempPath = join2(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
346
+ writeFileSync2(tempPath, next, "utf8");
347
+ if (process.platform === "win32") {
348
+ throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
349
+ }
350
+ await execa2("sudo", ["cp", tempPath, hostsPath], { stdio: "inherit" });
351
+ return tempPath;
352
+ }
353
+ async function updateSystemHosts(projectName, entries) {
354
+ const sanitizedProjectName = sanitizeProjectName(projectName);
355
+ const hostsPath = getSystemHostsPath();
356
+ const existing = readTextFile(hostsPath);
357
+ const block = renderHostsBlock(sanitizedProjectName, entries);
358
+ const next = upsertManagedBlock(existing, sanitizedProjectName, block);
359
+ if (next === existing) {
360
+ return { changed: false, hostsPath };
361
+ }
362
+ const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
363
+ return { changed: true, hostsPath, tempPath };
364
+ }
365
+
366
+ // src/prompt.ts
367
+ import { stdin as input, stdout as output } from "process";
368
+ import { createInterface } from "readline/promises";
369
+ function canPrompt() {
370
+ return Boolean(input.isTTY && output.isTTY);
371
+ }
372
+ async function withPrompt(run) {
373
+ const rl = createInterface({ input, output });
374
+ try {
375
+ return await run((question) => rl.question(question));
376
+ } finally {
377
+ rl.close();
378
+ }
379
+ }
380
+ async function confirm(question, defaultValue = true) {
381
+ return withPrompt(async (prompt) => {
382
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
383
+ const answer = (await prompt(`${question}${suffix}`)).trim().toLowerCase();
384
+ if (!answer) return defaultValue;
385
+ return answer === "y" || answer === "yes";
386
+ });
387
+ }
388
+ async function ask(question, defaultValue) {
389
+ return withPrompt(async (prompt) => {
390
+ const suffix = defaultValue ? ` (${defaultValue}) ` : " ";
391
+ const answer = (await prompt(`${question}${suffix}`)).trim();
392
+ return answer || defaultValue || "";
393
+ });
394
+ }
395
+
396
+ // src/state.ts
397
+ import { existsSync as existsSync3 } from "fs";
398
+ import { join as join3 } from "path";
399
+ var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
400
+ function getLocalghostStatePath(cwd = process.cwd()) {
401
+ return join3(cwd, LOCALGHOST_STATE_FILE);
402
+ }
403
+ function readLocalghostState(cwd = process.cwd()) {
404
+ const path = getLocalghostStatePath(cwd);
405
+ if (!existsSync3(path)) return null;
406
+ return JSON.parse(readTextFile(path));
407
+ }
408
+ function writeLocalghostState(cwd, state) {
409
+ const path = getLocalghostStatePath(cwd);
410
+ writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
411
+ `);
412
+ return path;
413
+ }
414
+
415
+ // src/caddy.ts
416
+ import { dirname as dirname2, join as join4 } from "path";
417
+ import { execa as execa3 } from "execa";
418
+ function groupByPort(entries) {
419
+ const groups = /* @__PURE__ */ new Map();
420
+ for (const entry of entries) {
421
+ const group = groups.get(entry.port) ?? [];
422
+ group.push(entry);
423
+ groups.set(entry.port, group);
424
+ }
425
+ return groups;
426
+ }
427
+ function getCaddyfilePath(cwd = process.cwd()) {
428
+ return join4(cwd, "ops/local/Caddyfile");
429
+ }
430
+ function renderCaddyfile(entries, options = {}) {
431
+ const groups = groupByPort(entries);
432
+ const https = options.https === true;
433
+ const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
434
+ const hosts = group.map((entry) => https ? entry.host : `http://${entry.host}`).sort().join(", ");
435
+ return `${hosts} {
436
+ reverse_proxy 127.0.0.1:${port}
437
+ }`;
438
+ });
439
+ const globalOptions = https ? `{
440
+ local_certs
441
+ }
442
+
443
+ ` : "";
444
+ return `${globalOptions}${blocks.join("\n\n")}
445
+ `;
446
+ }
447
+ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
448
+ const path = getCaddyfilePath(cwd);
449
+ writeTextFile(path, renderCaddyfile(entries, options));
450
+ return path;
451
+ }
452
+ async function validateCaddyfile(path) {
453
+ await execa3("caddy", ["validate", "--config", path], {
454
+ cwd: dirname2(path),
455
+ stdio: "inherit"
456
+ });
457
+ }
103
458
 
104
459
  // src/vite.ts
105
460
  function mergeAllowedHosts(current, hosts) {
@@ -126,7 +481,7 @@ function printLocalHosts(server, entries, vitePort, https) {
126
481
  const lines = [
127
482
  "",
128
483
  " localghost",
129
- ` open: ${primaryUrl}`,
484
+ ` local: ${primaryUrl}`,
130
485
  ...urls.slice(1).map((url) => ` also: ${url}`),
131
486
  vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
132
487
  https ? " proxy: Caddy local HTTPS" : void 0
@@ -144,29 +499,147 @@ function readOptionsFromPlugin(options) {
144
499
  ...options.configPattern ? { configPattern: options.configPattern } : {}
145
500
  };
146
501
  }
502
+ function getConfigWatchFiles(options) {
503
+ const readOptions = readOptionsFromPlugin(options);
504
+ const cwd = readOptions.cwd ?? process.cwd();
505
+ const resolvedPath = resolveDevHostsPath(readOptions);
506
+ const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve2(cwd, fileName));
507
+ const projectConfigPaths = options.localghostConfig === false ? [] : options.localghostConfig ? [resolve2(cwd, options.localghostConfig)] : ["localghost.config.mjs", "localghost.config.js", "localghost.config.cjs"].map((fileName) => resolve2(cwd, fileName));
508
+ return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path, ...projectConfigPaths])];
509
+ }
510
+ function normalizeWatchPath(filePath) {
511
+ return normalize(resolve2(filePath));
512
+ }
513
+ function renderConfig(hosts, port) {
514
+ return [
515
+ "# Buh. Friendly names for local services.",
516
+ "# Format: <host> <port>",
517
+ ...hosts.map((host) => `${host} ${port}`),
518
+ ""
519
+ ].join("\n");
520
+ }
521
+ function defaultHost(cwd) {
522
+ const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
523
+ return `${projectName}.localhost`;
524
+ }
525
+ async function promptForHosts(cwd, port) {
526
+ const primaryHost = await ask("Primary local domain", defaultHost(cwd));
527
+ const hosts = [primaryHost.toLowerCase()];
528
+ while (await confirm("Add another local domain?", false)) {
529
+ const host = await ask("Domain");
530
+ if (host) hosts.push(host.toLowerCase());
531
+ }
532
+ return [...new Set(addDefaultWwwAliases(hosts.map((host) => ({ host, port, target: `127.0.0.1:${port}` }))).map((entry) => entry.host))];
533
+ }
534
+ function hasReadySetup(cwd, entries, configPath, https) {
535
+ const state = readLocalghostState(cwd);
536
+ const projectName = sanitizeProjectName(getProjectName(cwd));
537
+ if (state?.action !== "setup" || state.configPath !== configPath) return false;
538
+ try {
539
+ const hosts = readFileSync3(getSystemHostsPath(), "utf8");
540
+ if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
541
+ } catch {
542
+ return false;
543
+ }
544
+ const caddyfilePath = getCaddyfilePath(cwd);
545
+ return existsSync4(caddyfilePath) && readFileSync3(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
546
+ }
547
+ async function setupProject(cwd, entries, configPath, https) {
548
+ const caddy = await checkCaddy();
549
+ if (!caddy.found) {
550
+ throw new Error([
551
+ "Caddy is missing.",
552
+ `Run: ${caddy.installHint}`,
553
+ "Localghost will not install it for you."
554
+ ].join("\n"));
555
+ }
556
+ const projectName = sanitizeProjectName(getProjectName(cwd));
557
+ console.log("Buh. macOS keeps local hostnames in /etc/hosts, so Localghost may ask for your password.");
558
+ console.log("It will only touch its managed Localghost block.");
559
+ const hostsResult = await updateSystemHosts(projectName, entries);
560
+ const caddyfilePath = await writeCaddyfile(entries, cwd, { https });
561
+ await validateCaddyfile(caddyfilePath);
562
+ writeLocalghostState(cwd, {
563
+ action: "setup",
564
+ projectName,
565
+ cwd,
566
+ configPath,
567
+ hostsPath: hostsResult.hostsPath,
568
+ hostsChanged: hostsResult.changed,
569
+ ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
570
+ caddyfilePath,
571
+ caddyHttps: https,
572
+ entries
573
+ });
574
+ }
575
+ async function ensureLocalghostContext(options, vitePort, https) {
576
+ const cwd = options.cwd ?? process.cwd();
577
+ const readOptions = readOptionsFromPlugin(options);
578
+ const resolved = resolveDevHostsPath(readOptions);
579
+ if (!resolved.exists) {
580
+ if (options.setup === false || !canPrompt()) {
581
+ throw new Error(
582
+ `No .localghost found at ${resolved.path}. Run \`localghost init --write-scripts\` or start Vite in an interactive terminal.`
583
+ );
584
+ }
585
+ console.log(`No .localghost found at ${resolved.path}.`);
586
+ if (!await confirm("Create one now?", true)) {
587
+ throw new Error("Localghost setup skipped. Create .localghost before running the Vite plugin.");
588
+ }
589
+ const hosts = await promptForHosts(cwd, vitePort);
590
+ writeTextFile(resolved.path, renderConfig(hosts, vitePort));
591
+ console.log(`Created ${resolved.path}`);
592
+ }
593
+ const context = await resolveLocalghostContext({
594
+ ...options,
595
+ cwd,
596
+ port: vitePort,
597
+ ...typeof https === "boolean" ? { https } : {}
598
+ });
599
+ if (!hasReadySetup(cwd, context.entries, resolved.path, context.https)) {
600
+ if (options.setup === false || !canPrompt()) return context;
601
+ const setup = await confirm("Run caddy:setup now?", true);
602
+ if (setup) {
603
+ await setupProject(cwd, context.entries, resolved.path, context.https);
604
+ console.log(`All set. Setup state: ${getLocalghostStatePath(cwd)}`);
605
+ }
606
+ }
607
+ return context;
608
+ }
147
609
  function localGhostPlugin(options = {}) {
148
610
  let resolvedEntries = [];
149
611
  let resolvedVitePort;
612
+ let resolvedHttps = false;
613
+ let restartTimer;
150
614
  return {
151
615
  name: "localghost:vite",
152
616
  enforce: "pre",
153
- config(userConfig) {
154
- const entries = readDevHosts(readOptionsFromPlugin(options));
155
- const hosts = [...new Set(entries.map((entry) => entry.host))];
617
+ async config(userConfig, configEnv) {
618
+ if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
619
+ return {};
620
+ }
156
621
  const existingServer = userConfig.server ?? {};
157
- const vitePort = options.port ?? existingServer.port ?? entries.find((entry) => !entry.host.startsWith("api."))?.port ?? entries[0]?.port;
158
- const primaryHost = options.primaryHost ?? entries.find((entry) => entry.port === vitePort)?.host ?? hosts[0];
622
+ const envVitePort = Number.parseInt(process.env.LOCALGHOST_PORT ?? process.env.VITE_PORT ?? "", 10);
623
+ const requestedVitePort = options.port ?? existingServer.port ?? (Number.isInteger(envVitePort) ? envVitePort : 5173);
624
+ const context = await ensureLocalghostContext(options, requestedVitePort, options.https);
625
+ const entries = context.entries;
626
+ const hosts = context.hosts;
627
+ const primaryHost = context.primaryHost;
159
628
  resolvedEntries = entries;
160
- resolvedVitePort = vitePort;
629
+ resolvedVitePort = context.port;
630
+ resolvedHttps = context.https;
161
631
  const server = {
162
632
  ...existingServer,
163
633
  allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
164
634
  strictPort: existingServer.strictPort ?? true
165
635
  };
166
- if (vitePort) {
167
- server.port = vitePort;
636
+ if (typeof existingServer.host === "undefined") {
637
+ server.host = context.bindHost;
168
638
  }
169
- if (options.https && primaryHost) {
639
+ if (context.port) {
640
+ server.port = context.port;
641
+ }
642
+ if (context.https && primaryHost) {
170
643
  const existingWs = typeof server.ws === "object" && server.ws ? server.ws : {};
171
644
  const existingHmr = typeof existingServer.hmr === "object" && existingServer.hmr ? existingServer.hmr : {};
172
645
  server.ws = {
@@ -185,12 +658,38 @@ function localGhostPlugin(options = {}) {
185
658
  return { server };
186
659
  },
187
660
  configureServer(server) {
188
- if (options.log === false) {
189
- return;
661
+ const watchFiles = getConfigWatchFiles(options);
662
+ const watchedConfigFiles = new Set(watchFiles.map(normalizeWatchPath));
663
+ server.watcher.add(watchFiles);
664
+ const restartOnLocalghostConfigChange = (filePath) => {
665
+ if (!watchedConfigFiles.has(normalizeWatchPath(filePath))) {
666
+ return;
667
+ }
668
+ if (restartTimer) {
669
+ clearTimeout(restartTimer);
670
+ }
671
+ restartTimer = setTimeout(() => {
672
+ if (options.log !== false) {
673
+ server.config.logger.info("localghost config changed; restarting Vite dev server", {
674
+ clear: false,
675
+ timestamp: false
676
+ });
677
+ }
678
+ void server.restart().catch((error) => {
679
+ server.config.logger.error(error instanceof Error ? error.message : String(error), {
680
+ timestamp: false
681
+ });
682
+ });
683
+ }, 50);
684
+ };
685
+ server.watcher.on("add", restartOnLocalghostConfigChange);
686
+ server.watcher.on("change", restartOnLocalghostConfigChange);
687
+ server.watcher.on("unlink", restartOnLocalghostConfigChange);
688
+ if (options.log !== false) {
689
+ server.printUrls = () => {
690
+ printLocalHosts(server, resolvedEntries, resolvedVitePort, resolvedHttps);
691
+ };
190
692
  }
191
- server.httpServer?.once("listening", () => {
192
- printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));
193
- });
194
693
  }
195
694
  };
196
695
  }