@hamedb89/localghost 0.1.3 → 0.1.6
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 +99 -14
- package/dist/cli.js +645 -83
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +102 -6
- package/dist/index.js +294 -50
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +460 -16
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +27 -5
- package/docs/github.md +5 -5
- package/docs/localghost.1.md +45 -9
- package/package.json +6 -4
package/dist/vite.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
// src/vite.ts
|
|
2
|
+
import { existsSync as existsSync3, 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(
|
|
11
|
+
function parseDevHosts(input2, fileName = ".localghost") {
|
|
8
12
|
const entries = [];
|
|
9
|
-
|
|
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,305 @@ 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/port.ts
|
|
122
|
+
import { createServer } from "net";
|
|
123
|
+
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
124
|
+
return new Promise((resolve3) => {
|
|
125
|
+
const server = createServer();
|
|
126
|
+
server.once("error", () => {
|
|
127
|
+
resolve3(false);
|
|
128
|
+
});
|
|
129
|
+
server.once("listening", () => {
|
|
130
|
+
server.close(() => resolve3(true));
|
|
131
|
+
});
|
|
132
|
+
server.listen(port, host);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function findAvailablePort(startPort, options = {}) {
|
|
136
|
+
const host = options.host ?? "127.0.0.1";
|
|
137
|
+
const maxAttempts = options.maxAttempts ?? 50;
|
|
138
|
+
for (let offset = 0; offset < maxAttempts; offset += 1) {
|
|
139
|
+
const port = startPort + offset;
|
|
140
|
+
if (await isPortAvailable(port, host)) {
|
|
141
|
+
return port;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/context.ts
|
|
148
|
+
function parsePort(value) {
|
|
149
|
+
if (!value) return void 0;
|
|
150
|
+
const port = Number.parseInt(value, 10);
|
|
151
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
152
|
+
}
|
|
153
|
+
function envPort() {
|
|
154
|
+
return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
|
|
155
|
+
}
|
|
156
|
+
function envDynamicPort() {
|
|
157
|
+
const value = process.env.LOCALGHOST_DYNAMIC_PORT;
|
|
158
|
+
if (!value) return void 0;
|
|
159
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
160
|
+
}
|
|
161
|
+
function readOptionsFromContext(options) {
|
|
162
|
+
return {
|
|
163
|
+
cwd: options.cwd ?? process.cwd(),
|
|
164
|
+
...options.fileName ? { fileName: options.fileName } : {},
|
|
165
|
+
...options.configFiles ? { configFiles: options.configFiles } : {},
|
|
166
|
+
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function withRuntimePort(entries, requestedPort, port) {
|
|
170
|
+
if (requestedPort === port) return entries;
|
|
171
|
+
const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
|
|
172
|
+
if (!hasRequestedPort) return entries;
|
|
173
|
+
return entries.map((entry) => entry.port === requestedPort ? { ...entry, port } : entry);
|
|
174
|
+
}
|
|
175
|
+
function uniqueHosts(entries) {
|
|
176
|
+
return [...new Set(entries.map((entry) => entry.host))];
|
|
177
|
+
}
|
|
178
|
+
async function resolveLocalghostContext(options = {}) {
|
|
179
|
+
const cwd = options.cwd ?? process.cwd();
|
|
180
|
+
const readOptions = readOptionsFromContext({ ...options, cwd });
|
|
181
|
+
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
182
|
+
const configEntries = readDevHosts(readOptions);
|
|
183
|
+
const requestedPort = options.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
184
|
+
const dynamicPort = options.dynamicPort ?? envDynamicPort() ?? false;
|
|
185
|
+
const bindHost = options.bindHost ?? "127.0.0.1";
|
|
186
|
+
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
187
|
+
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
188
|
+
const entries = withRuntimePort(configEntries, requestedPort, port);
|
|
189
|
+
const hosts = uniqueHosts(entries);
|
|
190
|
+
const primaryHost = options.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
191
|
+
return {
|
|
192
|
+
cwd,
|
|
193
|
+
projectName: sanitizeProjectName(options.project ?? getProjectName(cwd)),
|
|
194
|
+
readOptions,
|
|
195
|
+
configPath: resolvedPath.path,
|
|
196
|
+
configFileName: resolvedPath.fileName,
|
|
197
|
+
configEntries,
|
|
198
|
+
entries,
|
|
199
|
+
hosts,
|
|
200
|
+
requestedPort,
|
|
201
|
+
port,
|
|
202
|
+
dynamicPort,
|
|
203
|
+
bindHost,
|
|
204
|
+
primaryHost,
|
|
205
|
+
https: options.https === true
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/doctor.ts
|
|
210
|
+
import { execa } from "execa";
|
|
211
|
+
async function checkCaddy() {
|
|
212
|
+
try {
|
|
213
|
+
const result = await execa("caddy", ["version"], { reject: false });
|
|
214
|
+
const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
215
|
+
return {
|
|
216
|
+
found: result.exitCode === 0,
|
|
217
|
+
...version ? { version } : {},
|
|
218
|
+
installHint: "brew install caddy"
|
|
219
|
+
};
|
|
220
|
+
} catch {
|
|
221
|
+
return {
|
|
222
|
+
found: false,
|
|
223
|
+
installHint: "brew install caddy"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/env.ts
|
|
229
|
+
function getProductionReason(env = process.env) {
|
|
230
|
+
if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
|
|
231
|
+
if (env.NODE_ENV === "production") return "NODE_ENV=production";
|
|
232
|
+
if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
|
|
233
|
+
if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
|
|
234
|
+
if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
|
|
235
|
+
return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function isProductionLike(env = process.env) {
|
|
240
|
+
return getProductionReason(env) !== null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/fs.ts
|
|
244
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
245
|
+
import { dirname } from "path";
|
|
246
|
+
function readTextFile(path) {
|
|
247
|
+
return readFileSync2(path, "utf8");
|
|
248
|
+
}
|
|
249
|
+
function writeTextFile(path, value) {
|
|
250
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
251
|
+
writeFileSync(path, value, "utf8");
|
|
252
|
+
return path;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/hosts-file.ts
|
|
256
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
257
|
+
import { tmpdir } from "os";
|
|
258
|
+
import { join as join2 } from "path";
|
|
259
|
+
import { execa as execa2 } from "execa";
|
|
260
|
+
function escapeRegExp(value) {
|
|
261
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
262
|
+
}
|
|
263
|
+
function getManagedBlockPattern(projectName) {
|
|
264
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
265
|
+
const start = `# localghost:start ${sanitizedProjectName}`;
|
|
266
|
+
const end = `# localghost:end ${sanitizedProjectName}`;
|
|
267
|
+
return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
268
|
+
}
|
|
269
|
+
function getSystemHostsPath() {
|
|
270
|
+
return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
|
|
271
|
+
}
|
|
272
|
+
function renderHostsBlock(projectName, entries) {
|
|
273
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
274
|
+
const hosts = [...new Set(entries.map((entry) => entry.host))].sort();
|
|
275
|
+
return [
|
|
276
|
+
`# localghost:start ${sanitizedProjectName}`,
|
|
277
|
+
...hosts.map((host) => `127.0.0.1 ${host}`),
|
|
278
|
+
`# localghost:end ${sanitizedProjectName}`,
|
|
279
|
+
""
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
function upsertManagedBlock(existing, projectName, block) {
|
|
283
|
+
const pattern = getManagedBlockPattern(projectName);
|
|
284
|
+
if (pattern.test(existing)) {
|
|
285
|
+
return existing.replace(pattern, block);
|
|
286
|
+
}
|
|
287
|
+
return `${existing.trimEnd()}
|
|
288
|
+
|
|
289
|
+
${block}`;
|
|
290
|
+
}
|
|
291
|
+
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
292
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
293
|
+
const tempPath = join2(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
294
|
+
writeFileSync2(tempPath, next, "utf8");
|
|
295
|
+
if (process.platform === "win32") {
|
|
296
|
+
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
297
|
+
}
|
|
298
|
+
await execa2("sudo", ["cp", tempPath, hostsPath], { stdio: "inherit" });
|
|
299
|
+
return tempPath;
|
|
300
|
+
}
|
|
301
|
+
async function updateSystemHosts(projectName, entries) {
|
|
302
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
303
|
+
const hostsPath = getSystemHostsPath();
|
|
304
|
+
const existing = readTextFile(hostsPath);
|
|
305
|
+
const block = renderHostsBlock(sanitizedProjectName, entries);
|
|
306
|
+
const next = upsertManagedBlock(existing, sanitizedProjectName, block);
|
|
307
|
+
if (next === existing) {
|
|
308
|
+
return { changed: false, hostsPath };
|
|
309
|
+
}
|
|
310
|
+
const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
|
|
311
|
+
return { changed: true, hostsPath, tempPath };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/prompt.ts
|
|
315
|
+
import { stdin as input, stdout as output } from "process";
|
|
316
|
+
import { createInterface } from "readline/promises";
|
|
317
|
+
function canPrompt() {
|
|
318
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
319
|
+
}
|
|
320
|
+
async function withPrompt(run) {
|
|
321
|
+
const rl = createInterface({ input, output });
|
|
322
|
+
try {
|
|
323
|
+
return await run((question) => rl.question(question));
|
|
324
|
+
} finally {
|
|
325
|
+
rl.close();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function confirm(question, defaultValue = true) {
|
|
329
|
+
return withPrompt(async (prompt) => {
|
|
330
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
331
|
+
const answer = (await prompt(`${question}${suffix}`)).trim().toLowerCase();
|
|
332
|
+
if (!answer) return defaultValue;
|
|
333
|
+
return answer === "y" || answer === "yes";
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async function ask(question, defaultValue) {
|
|
337
|
+
return withPrompt(async (prompt) => {
|
|
338
|
+
const suffix = defaultValue ? ` (${defaultValue}) ` : " ";
|
|
339
|
+
const answer = (await prompt(`${question}${suffix}`)).trim();
|
|
340
|
+
return answer || defaultValue || "";
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/state.ts
|
|
345
|
+
import { existsSync as existsSync2 } from "fs";
|
|
346
|
+
import { join as join3 } from "path";
|
|
347
|
+
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
348
|
+
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
349
|
+
return join3(cwd, LOCALGHOST_STATE_FILE);
|
|
350
|
+
}
|
|
351
|
+
function readLocalghostState(cwd = process.cwd()) {
|
|
352
|
+
const path = getLocalghostStatePath(cwd);
|
|
353
|
+
if (!existsSync2(path)) return null;
|
|
354
|
+
return JSON.parse(readTextFile(path));
|
|
355
|
+
}
|
|
356
|
+
function writeLocalghostState(cwd, state) {
|
|
357
|
+
const path = getLocalghostStatePath(cwd);
|
|
358
|
+
writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...state }, null, 2)}
|
|
359
|
+
`);
|
|
360
|
+
return path;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/caddy.ts
|
|
364
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
365
|
+
import { execa as execa3 } from "execa";
|
|
366
|
+
function groupByPort(entries) {
|
|
367
|
+
const groups = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
const group = groups.get(entry.port) ?? [];
|
|
370
|
+
group.push(entry);
|
|
371
|
+
groups.set(entry.port, group);
|
|
372
|
+
}
|
|
373
|
+
return groups;
|
|
374
|
+
}
|
|
375
|
+
function getCaddyfilePath(cwd = process.cwd()) {
|
|
376
|
+
return join4(cwd, "ops/local/Caddyfile");
|
|
377
|
+
}
|
|
378
|
+
function renderCaddyfile(entries, options = {}) {
|
|
379
|
+
const groups = groupByPort(entries);
|
|
380
|
+
const https = options.https === true;
|
|
381
|
+
const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
|
|
382
|
+
const hosts = group.map((entry) => https ? entry.host : `http://${entry.host}`).sort().join(", ");
|
|
383
|
+
return `${hosts} {
|
|
384
|
+
reverse_proxy 127.0.0.1:${port}
|
|
385
|
+
}`;
|
|
386
|
+
});
|
|
387
|
+
const globalOptions = https ? `{
|
|
388
|
+
local_certs
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
` : "";
|
|
392
|
+
return `${globalOptions}${blocks.join("\n\n")}
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
|
|
396
|
+
const path = getCaddyfilePath(cwd);
|
|
397
|
+
writeTextFile(path, renderCaddyfile(entries, options));
|
|
398
|
+
return path;
|
|
399
|
+
}
|
|
400
|
+
async function validateCaddyfile(path) {
|
|
401
|
+
await execa3("caddy", ["validate", "--config", path], {
|
|
402
|
+
cwd: dirname2(path),
|
|
403
|
+
stdio: "inherit"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
103
406
|
|
|
104
407
|
// src/vite.ts
|
|
105
408
|
function mergeAllowedHosts(current, hosts) {
|
|
@@ -126,7 +429,7 @@ function printLocalHosts(server, entries, vitePort, https) {
|
|
|
126
429
|
const lines = [
|
|
127
430
|
"",
|
|
128
431
|
" localghost",
|
|
129
|
-
`
|
|
432
|
+
` local: ${primaryUrl}`,
|
|
130
433
|
...urls.slice(1).map((url) => ` also: ${url}`),
|
|
131
434
|
vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
|
|
132
435
|
https ? " proxy: Caddy local HTTPS" : void 0
|
|
@@ -144,27 +447,142 @@ function readOptionsFromPlugin(options) {
|
|
|
144
447
|
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
145
448
|
};
|
|
146
449
|
}
|
|
450
|
+
function getConfigWatchFiles(options) {
|
|
451
|
+
const readOptions = readOptionsFromPlugin(options);
|
|
452
|
+
const cwd = readOptions.cwd ?? process.cwd();
|
|
453
|
+
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
454
|
+
const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve2(cwd, fileName));
|
|
455
|
+
return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path])];
|
|
456
|
+
}
|
|
457
|
+
function normalizeWatchPath(filePath) {
|
|
458
|
+
return normalize(resolve2(filePath));
|
|
459
|
+
}
|
|
460
|
+
function renderConfig(hosts, port) {
|
|
461
|
+
return [
|
|
462
|
+
"# Buh. Friendly names for local services.",
|
|
463
|
+
"# Format: <host> <port>",
|
|
464
|
+
...hosts.map((host) => `${host} ${port}`),
|
|
465
|
+
""
|
|
466
|
+
].join("\n");
|
|
467
|
+
}
|
|
468
|
+
function defaultHost(cwd) {
|
|
469
|
+
const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
|
|
470
|
+
return `${projectName}.localhost`;
|
|
471
|
+
}
|
|
472
|
+
async function promptForHosts(cwd, port) {
|
|
473
|
+
const primaryHost = await ask("Primary local domain", defaultHost(cwd));
|
|
474
|
+
const hosts = [primaryHost.toLowerCase()];
|
|
475
|
+
while (await confirm("Add another local domain?", false)) {
|
|
476
|
+
const host = await ask("Domain");
|
|
477
|
+
if (host) hosts.push(host.toLowerCase());
|
|
478
|
+
}
|
|
479
|
+
return [...new Set(hosts)];
|
|
480
|
+
}
|
|
481
|
+
function hasReadySetup(cwd, entries, configPath, https) {
|
|
482
|
+
const state = readLocalghostState(cwd);
|
|
483
|
+
const projectName = sanitizeProjectName(getProjectName(cwd));
|
|
484
|
+
if (state?.action !== "setup" || state.configPath !== configPath) return false;
|
|
485
|
+
try {
|
|
486
|
+
const hosts = readFileSync3(getSystemHostsPath(), "utf8");
|
|
487
|
+
if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
|
|
488
|
+
} catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const caddyfilePath = getCaddyfilePath(cwd);
|
|
492
|
+
return existsSync3(caddyfilePath) && readFileSync3(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
|
|
493
|
+
}
|
|
494
|
+
async function setupProject(cwd, entries, configPath, https) {
|
|
495
|
+
const caddy = await checkCaddy();
|
|
496
|
+
if (!caddy.found) {
|
|
497
|
+
throw new Error([
|
|
498
|
+
"Caddy is missing.",
|
|
499
|
+
`Run: ${caddy.installHint}`,
|
|
500
|
+
"Localghost will not install it for you."
|
|
501
|
+
].join("\n"));
|
|
502
|
+
}
|
|
503
|
+
const projectName = sanitizeProjectName(getProjectName(cwd));
|
|
504
|
+
console.log("Buh. macOS keeps local hostnames in /etc/hosts, so Localghost may ask for your password.");
|
|
505
|
+
console.log("It will only touch its managed Localghost block.");
|
|
506
|
+
const hostsResult = await updateSystemHosts(projectName, entries);
|
|
507
|
+
const caddyfilePath = await writeCaddyfile(entries, cwd, { https });
|
|
508
|
+
await validateCaddyfile(caddyfilePath);
|
|
509
|
+
writeLocalghostState(cwd, {
|
|
510
|
+
action: "setup",
|
|
511
|
+
projectName,
|
|
512
|
+
cwd,
|
|
513
|
+
configPath,
|
|
514
|
+
hostsPath: hostsResult.hostsPath,
|
|
515
|
+
hostsChanged: hostsResult.changed,
|
|
516
|
+
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
517
|
+
caddyfilePath,
|
|
518
|
+
caddyHttps: https,
|
|
519
|
+
entries
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async function ensureLocalghostContext(options, vitePort, https) {
|
|
523
|
+
const cwd = options.cwd ?? process.cwd();
|
|
524
|
+
const readOptions = readOptionsFromPlugin(options);
|
|
525
|
+
const resolved = resolveDevHostsPath(readOptions);
|
|
526
|
+
if (!resolved.exists) {
|
|
527
|
+
if (options.setup === false || !canPrompt()) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`No .localghost found at ${resolved.path}. Run \`localghost init --write-scripts\` or start Vite in an interactive terminal.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
console.log(`No .localghost found at ${resolved.path}.`);
|
|
533
|
+
if (!await confirm("Create one now?", true)) {
|
|
534
|
+
throw new Error("Localghost setup skipped. Create .localghost before running the Vite plugin.");
|
|
535
|
+
}
|
|
536
|
+
const hosts = await promptForHosts(cwd, vitePort);
|
|
537
|
+
writeTextFile(resolved.path, renderConfig(hosts, vitePort));
|
|
538
|
+
console.log(`Created ${resolved.path}`);
|
|
539
|
+
}
|
|
540
|
+
const context = await resolveLocalghostContext({
|
|
541
|
+
...options,
|
|
542
|
+
cwd,
|
|
543
|
+
port: vitePort,
|
|
544
|
+
https
|
|
545
|
+
});
|
|
546
|
+
if (!hasReadySetup(cwd, context.entries, resolved.path, https)) {
|
|
547
|
+
if (options.setup === false || !canPrompt()) return context;
|
|
548
|
+
const setup = await confirm("Run caddy:setup now?", true);
|
|
549
|
+
if (setup) {
|
|
550
|
+
await setupProject(cwd, context.entries, resolved.path, https);
|
|
551
|
+
console.log(`All set. Setup state: ${getLocalghostStatePath(cwd)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return context;
|
|
555
|
+
}
|
|
147
556
|
function localGhostPlugin(options = {}) {
|
|
148
557
|
let resolvedEntries = [];
|
|
149
558
|
let resolvedVitePort;
|
|
559
|
+
let restartTimer;
|
|
150
560
|
return {
|
|
151
561
|
name: "localghost:vite",
|
|
152
562
|
enforce: "pre",
|
|
153
|
-
config(userConfig) {
|
|
154
|
-
|
|
155
|
-
|
|
563
|
+
async config(userConfig, configEnv) {
|
|
564
|
+
if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
|
|
565
|
+
return {};
|
|
566
|
+
}
|
|
156
567
|
const existingServer = userConfig.server ?? {};
|
|
157
|
-
const
|
|
158
|
-
const
|
|
568
|
+
const envVitePort = Number.parseInt(process.env.LOCALGHOST_PORT ?? process.env.VITE_PORT ?? "", 10);
|
|
569
|
+
const requestedVitePort = options.port ?? existingServer.port ?? (Number.isInteger(envVitePort) ? envVitePort : 5173);
|
|
570
|
+
const context = await ensureLocalghostContext(options, requestedVitePort, Boolean(options.https));
|
|
571
|
+
const entries = context.entries;
|
|
572
|
+
const hosts = context.hosts;
|
|
573
|
+
const primaryHost = context.primaryHost;
|
|
159
574
|
resolvedEntries = entries;
|
|
160
|
-
resolvedVitePort =
|
|
575
|
+
resolvedVitePort = context.port;
|
|
161
576
|
const server = {
|
|
162
577
|
...existingServer,
|
|
163
578
|
allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
|
|
164
579
|
strictPort: existingServer.strictPort ?? true
|
|
165
580
|
};
|
|
166
|
-
if (
|
|
167
|
-
server.
|
|
581
|
+
if (typeof existingServer.host === "undefined") {
|
|
582
|
+
server.host = context.bindHost;
|
|
583
|
+
}
|
|
584
|
+
if (context.port) {
|
|
585
|
+
server.port = context.port;
|
|
168
586
|
}
|
|
169
587
|
if (options.https && primaryHost) {
|
|
170
588
|
const existingWs = typeof server.ws === "object" && server.ws ? server.ws : {};
|
|
@@ -185,12 +603,38 @@ function localGhostPlugin(options = {}) {
|
|
|
185
603
|
return { server };
|
|
186
604
|
},
|
|
187
605
|
configureServer(server) {
|
|
188
|
-
|
|
189
|
-
|
|
606
|
+
const watchFiles = getConfigWatchFiles(options);
|
|
607
|
+
const watchedConfigFiles = new Set(watchFiles.map(normalizeWatchPath));
|
|
608
|
+
server.watcher.add(watchFiles);
|
|
609
|
+
const restartOnLocalghostConfigChange = (filePath) => {
|
|
610
|
+
if (!watchedConfigFiles.has(normalizeWatchPath(filePath))) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (restartTimer) {
|
|
614
|
+
clearTimeout(restartTimer);
|
|
615
|
+
}
|
|
616
|
+
restartTimer = setTimeout(() => {
|
|
617
|
+
if (options.log !== false) {
|
|
618
|
+
server.config.logger.info("localghost config changed; restarting Vite dev server", {
|
|
619
|
+
clear: false,
|
|
620
|
+
timestamp: false
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
void server.restart().catch((error) => {
|
|
624
|
+
server.config.logger.error(error instanceof Error ? error.message : String(error), {
|
|
625
|
+
timestamp: false
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}, 50);
|
|
629
|
+
};
|
|
630
|
+
server.watcher.on("add", restartOnLocalghostConfigChange);
|
|
631
|
+
server.watcher.on("change", restartOnLocalghostConfigChange);
|
|
632
|
+
server.watcher.on("unlink", restartOnLocalghostConfigChange);
|
|
633
|
+
if (options.log !== false) {
|
|
634
|
+
server.printUrls = () => {
|
|
635
|
+
printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));
|
|
636
|
+
};
|
|
190
637
|
}
|
|
191
|
-
server.httpServer?.once("listening", () => {
|
|
192
|
-
printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));
|
|
193
|
-
});
|
|
194
638
|
}
|
|
195
639
|
};
|
|
196
640
|
}
|