@hamedb89/localghost 0.1.6 → 0.1.9
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 +202 -22
- package/apps/macos-widget/LocalghostWidget.entitlements +12 -0
- package/apps/macos-widget/LocalghostWidget.swift +871 -0
- package/apps/macos-widget/Resources/localghost-logo-source.png +0 -0
- package/apps/macos-widget/Resources/localghost-widget-ui-reference.png +0 -0
- package/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift +50 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift +176 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidgetExtension.entitlements +12 -0
- package/apps/macos-widget/build.sh +60 -0
- package/apps/macos-widget/project.yml +40 -0
- package/assets/ghost-tunnel-app-icon.png +0 -0
- package/assets/ghost-tunnel-portal.png +0 -0
- package/assets/ghost-tunnel-wordmark.png +0 -0
- package/dist/cli.js +731 -91
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +138 -5
- package/dist/index.js +833 -37
- package/dist/index.js.map +1 -1
- package/dist/tunnel-DzfLXZ8O.d.ts +126 -0
- package/dist/vite.d.ts +5 -1
- package/dist/vite.js +717 -43
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +249 -0
- package/docs/github.md +6 -6
- package/docs/localghost.1.md +28 -7
- package/docs/macos-widget.md +108 -0
- package/package.json +8 -2
- package/dist/config-Cde1Bich.d.ts +0 -31
package/dist/vite.js
CHANGED
|
@@ -1,10 +1,125 @@
|
|
|
1
1
|
// src/vite.ts
|
|
2
|
-
import { existsSync as
|
|
2
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
3
3
|
import { normalize, resolve as resolve2 } from "path";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { emitKeypressEvents } from "readline";
|
|
6
|
+
|
|
7
|
+
// src/activity.ts
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
var LOCALGHOST_ACTIVITY_VERSION = 1;
|
|
12
|
+
function getLocalghostActivityPath(env = process.env) {
|
|
13
|
+
if (env.LOCALGHOST_ACTIVITY_PATH) return env.LOCALGHOST_ACTIVITY_PATH;
|
|
14
|
+
const stateRoot = env.XDG_STATE_HOME || join(homedir(), ".local/state");
|
|
15
|
+
return join(stateRoot, "localghost", "activity.json");
|
|
16
|
+
}
|
|
17
|
+
function isProcessRunning(pid) {
|
|
18
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
24
|
+
return code === "EPERM";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function emptyActivity() {
|
|
28
|
+
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
|
|
29
|
+
}
|
|
30
|
+
function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
31
|
+
if (!existsSync(path)) return emptyActivity();
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
+
return {
|
|
35
|
+
version: LOCALGHOST_ACTIVITY_VERSION,
|
|
36
|
+
runs: Array.isArray(parsed.runs) ? parsed.runs : [],
|
|
37
|
+
setups: Array.isArray(parsed.setups) ? parsed.setups : []
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return emptyActivity();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
writeFileSync(path, `${JSON.stringify(activity, null, 2)}
|
|
46
|
+
`, "utf8");
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
function createRunId(input2, pid) {
|
|
50
|
+
return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
|
|
51
|
+
}
|
|
52
|
+
function createSetupId(input2) {
|
|
53
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
56
|
+
const activity = readLocalghostActivity(path);
|
|
57
|
+
const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
|
|
58
|
+
const pruned = activeRuns.length !== activity.runs.length;
|
|
59
|
+
if (pruned) {
|
|
60
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
path,
|
|
64
|
+
pruned,
|
|
65
|
+
runs: activeRuns,
|
|
66
|
+
setups: activity.setups
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
70
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
71
|
+
const pid = input2.pid ?? process.pid;
|
|
72
|
+
const record = {
|
|
73
|
+
id: input2.id ?? createRunId(input2, pid),
|
|
74
|
+
mode: input2.mode,
|
|
75
|
+
pid,
|
|
76
|
+
cwd: input2.cwd,
|
|
77
|
+
projectName: input2.projectName,
|
|
78
|
+
startedAt: input2.startedAt ?? now,
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
81
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
82
|
+
...input2.caddyPid ? { caddyPid: input2.caddyPid } : {},
|
|
83
|
+
...input2.childPid ? { childPid: input2.childPid } : {},
|
|
84
|
+
...input2.childCommand ? { childCommand: input2.childCommand } : {},
|
|
85
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
86
|
+
...input2.requestedPort ? { requestedPort: input2.requestedPort } : {},
|
|
87
|
+
...input2.port ? { port: input2.port } : {},
|
|
88
|
+
...typeof input2.dynamicPort === "boolean" ? { dynamicPort: input2.dynamicPort } : {},
|
|
89
|
+
entries: input2.entries
|
|
90
|
+
};
|
|
91
|
+
const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
|
|
92
|
+
const activity = readLocalghostActivity(path);
|
|
93
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
|
|
97
|
+
const record = {
|
|
98
|
+
id: input2.id ?? createSetupId(input2),
|
|
99
|
+
cwd: input2.cwd,
|
|
100
|
+
projectName: input2.projectName,
|
|
101
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
103
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
104
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
105
|
+
entries: input2.entries
|
|
106
|
+
};
|
|
107
|
+
const activity = pruneLocalghostActivity(path);
|
|
108
|
+
const setups = activity.setups.filter((setup) => setup.id !== record.id);
|
|
109
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
|
|
113
|
+
const activity = readLocalghostActivity(path);
|
|
114
|
+
const runs = activity.runs.filter((run) => run.id !== id);
|
|
115
|
+
if (runs.length !== activity.runs.length) {
|
|
116
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
4
119
|
|
|
5
120
|
// src/config.ts
|
|
6
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
7
|
-
import { basename, join, resolve } from "path";
|
|
121
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
122
|
+
import { basename, join as join2, resolve } from "path";
|
|
8
123
|
|
|
9
124
|
// src/parse.ts
|
|
10
125
|
var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
|
|
@@ -69,7 +184,7 @@ function resolveDevHostsPath(options = {}) {
|
|
|
69
184
|
const searchedFiles = getConfigFileCandidates(options);
|
|
70
185
|
for (const fileName2 of searchedFiles) {
|
|
71
186
|
const path = resolve(cwd, fileName2);
|
|
72
|
-
if (
|
|
187
|
+
if (existsSync2(path)) {
|
|
73
188
|
return {
|
|
74
189
|
path,
|
|
75
190
|
fileName: basename(fileName2),
|
|
@@ -102,11 +217,11 @@ function readDevHosts(options = {}) {
|
|
|
102
217
|
`Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
|
|
103
218
|
);
|
|
104
219
|
}
|
|
105
|
-
return parseDevHosts(
|
|
220
|
+
return parseDevHosts(readFileSync2(resolvedPath.path, "utf8"), resolvedPath.fileName);
|
|
106
221
|
}
|
|
107
222
|
function getProjectName(cwd = process.cwd()) {
|
|
108
223
|
try {
|
|
109
|
-
const pkg = JSON.parse(
|
|
224
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
110
225
|
const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
|
|
111
226
|
return sanitizeProjectName(name.replace(/^@/, ""));
|
|
112
227
|
} catch {
|
|
@@ -118,6 +233,11 @@ function sanitizeProjectName(value) {
|
|
|
118
233
|
return projectName || "app";
|
|
119
234
|
}
|
|
120
235
|
|
|
236
|
+
// src/context.ts
|
|
237
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
238
|
+
import { join as join3 } from "path";
|
|
239
|
+
import { pathToFileURL } from "url";
|
|
240
|
+
|
|
121
241
|
// src/port.ts
|
|
122
242
|
import { createServer } from "net";
|
|
123
243
|
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
@@ -144,7 +264,263 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
144
264
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
145
265
|
}
|
|
146
266
|
|
|
267
|
+
// src/tunnel.ts
|
|
268
|
+
import { domainToASCII } from "url";
|
|
269
|
+
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
270
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
271
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
272
|
+
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
273
|
+
function isResolvedGhostTunnelConfig(value) {
|
|
274
|
+
return typeof value === "object" && value !== null && "enabled" in value;
|
|
275
|
+
}
|
|
276
|
+
function toGhostTunnelConfig(options) {
|
|
277
|
+
return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
|
|
278
|
+
}
|
|
279
|
+
function stripHostPort(value) {
|
|
280
|
+
const trimmed = value.trim().toLowerCase();
|
|
281
|
+
if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
|
|
282
|
+
const portSeparator = trimmed.lastIndexOf(":");
|
|
283
|
+
if (portSeparator === -1) return trimmed;
|
|
284
|
+
const port = trimmed.slice(portSeparator + 1);
|
|
285
|
+
return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
|
|
286
|
+
}
|
|
287
|
+
function normalizeDomain(value) {
|
|
288
|
+
const host = stripHostPort(value.replace(/^\*\./, ""));
|
|
289
|
+
const ascii = domainToASCII(host);
|
|
290
|
+
if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
|
|
291
|
+
if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
|
|
292
|
+
if (ascii.includes("*")) return null;
|
|
293
|
+
if (!ascii.split(".").every(isValidHostLabel)) return null;
|
|
294
|
+
return ascii;
|
|
295
|
+
}
|
|
296
|
+
function isValidHostLabel(value) {
|
|
297
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
298
|
+
}
|
|
299
|
+
function isValidNamespaceTag(value) {
|
|
300
|
+
return /^[a-z][a-z0-9_]*$/i.test(value);
|
|
301
|
+
}
|
|
302
|
+
function isNamespaceTagList(options) {
|
|
303
|
+
return Array.isArray(options);
|
|
304
|
+
}
|
|
305
|
+
function assertValidSubdomain(value) {
|
|
306
|
+
if (!isValidHostLabel(value)) {
|
|
307
|
+
throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function normalizeDomains(domains) {
|
|
311
|
+
const values = typeof domains === "string" ? [domains] : [...domains ?? []];
|
|
312
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
|
|
313
|
+
const domain = normalizeDomain(value);
|
|
314
|
+
if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
|
|
315
|
+
return domain;
|
|
316
|
+
});
|
|
317
|
+
return [...new Set(normalized)];
|
|
318
|
+
}
|
|
319
|
+
function parseGhostTunnelMode(value) {
|
|
320
|
+
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
321
|
+
}
|
|
322
|
+
function resolveNamespaceConfig(options) {
|
|
323
|
+
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
324
|
+
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
325
|
+
let spreadTag = tags.includes("project") ? "project" : void 0;
|
|
326
|
+
if (options && !isNamespaceTagList(options)) {
|
|
327
|
+
separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
328
|
+
spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
|
|
329
|
+
}
|
|
330
|
+
if (tags.length === 0) {
|
|
331
|
+
throw new Error("Ghost tunnel namespace must include at least one tag.");
|
|
332
|
+
}
|
|
333
|
+
for (const tag of tags) {
|
|
334
|
+
if (!isValidNamespaceTag(tag)) {
|
|
335
|
+
throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (spreadTag && !tags.includes(spreadTag)) {
|
|
339
|
+
throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
|
|
340
|
+
}
|
|
341
|
+
if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
|
|
342
|
+
throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
tags,
|
|
346
|
+
separator,
|
|
347
|
+
...spreadTag ? { spreadTag } : {}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizeNamespaceValue(tag, value, separator, options = {}) {
|
|
351
|
+
const normalized = normalizeDomain(value);
|
|
352
|
+
if (!normalized || normalized.includes(".")) {
|
|
353
|
+
throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
|
|
354
|
+
}
|
|
355
|
+
if (!options.allowSeparator && normalized.includes(separator)) {
|
|
356
|
+
throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
|
|
357
|
+
}
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
function createNamespaceSlug(config, values) {
|
|
361
|
+
const parts = config.tags.map((tag) => {
|
|
362
|
+
const value = values[tag];
|
|
363
|
+
if (!value) {
|
|
364
|
+
throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
|
|
365
|
+
}
|
|
366
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
367
|
+
});
|
|
368
|
+
const slug = parts.join(config.separator);
|
|
369
|
+
if (!isValidHostLabel(slug)) {
|
|
370
|
+
throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
|
|
371
|
+
}
|
|
372
|
+
return slug;
|
|
373
|
+
}
|
|
374
|
+
function createNamespaceDisplaySlug(config, values = {}) {
|
|
375
|
+
return config.tags.map((tag) => {
|
|
376
|
+
const value = values[tag];
|
|
377
|
+
if (!value) return `<${tag}>`;
|
|
378
|
+
try {
|
|
379
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
380
|
+
} catch {
|
|
381
|
+
return `<${tag}>`;
|
|
382
|
+
}
|
|
383
|
+
}).join(config.separator);
|
|
384
|
+
}
|
|
385
|
+
function getPreviewDefaults(preview, defaults) {
|
|
386
|
+
return {
|
|
387
|
+
domain: preview?.domain ?? defaults?.domain,
|
|
388
|
+
route: preview?.route ?? defaults?.route,
|
|
389
|
+
project: preview?.project ?? defaults?.project,
|
|
390
|
+
owner: preview?.owner ?? defaults?.owner,
|
|
391
|
+
values: {
|
|
392
|
+
...defaults?.values ?? {},
|
|
393
|
+
...preview?.values ?? {}
|
|
394
|
+
},
|
|
395
|
+
path: preview?.path,
|
|
396
|
+
protocol: preview?.protocol
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function getDisplayValues(input2) {
|
|
400
|
+
return {
|
|
401
|
+
...input2.route ? { route: input2.route } : {},
|
|
402
|
+
...input2.project ? { project: input2.project } : {},
|
|
403
|
+
...input2.owner ? { owner: input2.owner } : {},
|
|
404
|
+
...input2.values
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
408
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
409
|
+
const protocol = input2.protocol ?? "https";
|
|
410
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
411
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
412
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
413
|
+
if (!input2.path) return url;
|
|
414
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
415
|
+
}
|
|
416
|
+
function createDisplayUrls(config, defaults) {
|
|
417
|
+
const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
|
|
418
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
|
|
419
|
+
return [...new Set(urls)];
|
|
420
|
+
}
|
|
421
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
422
|
+
if (!config.preview) return void 0;
|
|
423
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
424
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
425
|
+
return constructGhostTunnelUrl({
|
|
426
|
+
domain: input2.domain,
|
|
427
|
+
route: input2.route,
|
|
428
|
+
project: input2.project,
|
|
429
|
+
owner: input2.owner,
|
|
430
|
+
values: input2.values,
|
|
431
|
+
...input2.path ? { path: input2.path } : {},
|
|
432
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
433
|
+
ghostTunnel: config
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
437
|
+
if (options === false || typeof options === "undefined") {
|
|
438
|
+
return {
|
|
439
|
+
enabled: false,
|
|
440
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
441
|
+
domains: [],
|
|
442
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
443
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
444
|
+
displayUrls: [],
|
|
445
|
+
requireHttps: true,
|
|
446
|
+
requireAuth: true
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
450
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
451
|
+
assertValidSubdomain(subdomain);
|
|
452
|
+
const domains = normalizeDomains(config.domains);
|
|
453
|
+
const enabled = config.enabled ?? true;
|
|
454
|
+
const resolved = {
|
|
455
|
+
enabled,
|
|
456
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
457
|
+
domains,
|
|
458
|
+
subdomain,
|
|
459
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
460
|
+
...config.preview ? { preview: config.preview } : {},
|
|
461
|
+
displayUrls: [],
|
|
462
|
+
requireHttps: config.requireHttps ?? true,
|
|
463
|
+
requireAuth: config.requireAuth ?? true
|
|
464
|
+
};
|
|
465
|
+
if (!enabled) {
|
|
466
|
+
return resolved;
|
|
467
|
+
}
|
|
468
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
469
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
470
|
+
return {
|
|
471
|
+
...resolved,
|
|
472
|
+
displayUrls,
|
|
473
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
474
|
+
...previewUrl ? { previewUrl } : {}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
478
|
+
const config = toGhostTunnelConfig(options);
|
|
479
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
480
|
+
if (!normalizedDomain) {
|
|
481
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
482
|
+
}
|
|
483
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
484
|
+
}
|
|
485
|
+
function constructGhostTunnelHost(input2) {
|
|
486
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
487
|
+
if (!config.enabled) {
|
|
488
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
489
|
+
}
|
|
490
|
+
const namespaceValues = {
|
|
491
|
+
route: input2.route,
|
|
492
|
+
project: input2.project,
|
|
493
|
+
owner: input2.owner,
|
|
494
|
+
...input2.values ?? {}
|
|
495
|
+
};
|
|
496
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
497
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
498
|
+
}
|
|
499
|
+
function constructGhostTunnelUrl(input2) {
|
|
500
|
+
const protocol = input2.protocol ?? "https";
|
|
501
|
+
const host = constructGhostTunnelHost(input2);
|
|
502
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
503
|
+
if (input2.path) {
|
|
504
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
505
|
+
}
|
|
506
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
507
|
+
url.search = input2.searchParams.toString();
|
|
508
|
+
} else if (input2.searchParams) {
|
|
509
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
510
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
511
|
+
url.searchParams.set(key, String(value));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return url.toString();
|
|
516
|
+
}
|
|
517
|
+
|
|
147
518
|
// src/context.ts
|
|
519
|
+
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
520
|
+
"localghost.config.mjs",
|
|
521
|
+
"localghost.config.js",
|
|
522
|
+
"localghost.config.cjs"
|
|
523
|
+
];
|
|
148
524
|
function parsePort(value) {
|
|
149
525
|
if (!value) return void 0;
|
|
150
526
|
const port = Number.parseInt(value, 10);
|
|
@@ -158,6 +534,30 @@ function envDynamicPort() {
|
|
|
158
534
|
if (!value) return void 0;
|
|
159
535
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
160
536
|
}
|
|
537
|
+
function envHttps() {
|
|
538
|
+
const value = process.env.LOCALGHOST_HTTPS;
|
|
539
|
+
if (!value) return void 0;
|
|
540
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
541
|
+
}
|
|
542
|
+
function getPackageName(cwd) {
|
|
543
|
+
try {
|
|
544
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
545
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
546
|
+
} catch {
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function getPackageOwner(cwd) {
|
|
551
|
+
const packageName = getPackageName(cwd);
|
|
552
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
553
|
+
return packageName.slice(1).split("/")[0];
|
|
554
|
+
}
|
|
555
|
+
function getLocalOwner(cwd) {
|
|
556
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
557
|
+
}
|
|
558
|
+
function getRouteName(primaryHost, fallback) {
|
|
559
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
560
|
+
}
|
|
161
561
|
function readOptionsFromContext(options) {
|
|
162
562
|
return {
|
|
163
563
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -175,22 +575,68 @@ function withRuntimePort(entries, requestedPort, port) {
|
|
|
175
575
|
function uniqueHosts(entries) {
|
|
176
576
|
return [...new Set(entries.map((entry) => entry.host))];
|
|
177
577
|
}
|
|
578
|
+
function isAliasableHost(host) {
|
|
579
|
+
return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
|
|
580
|
+
}
|
|
581
|
+
function getDefaultWwwAlias(host) {
|
|
582
|
+
return isAliasableHost(host) ? `www.${host}` : null;
|
|
583
|
+
}
|
|
584
|
+
function addDefaultWwwAliases(entries) {
|
|
585
|
+
const seen = new Set(entries.map((entry) => entry.host));
|
|
586
|
+
const aliases = [];
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
const alias = getDefaultWwwAlias(entry.host);
|
|
589
|
+
if (alias && !seen.has(alias)) {
|
|
590
|
+
aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
|
|
591
|
+
seen.add(alias);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return [...entries, ...aliases];
|
|
595
|
+
}
|
|
596
|
+
function defined(input2) {
|
|
597
|
+
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
598
|
+
}
|
|
599
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
600
|
+
const cwd = options.cwd ?? process.cwd();
|
|
601
|
+
if (options.configFile === false) return { config: {} };
|
|
602
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
603
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
604
|
+
if (!path) return { config: {} };
|
|
605
|
+
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
606
|
+
const config = imported.default ?? imported;
|
|
607
|
+
return { config, path };
|
|
608
|
+
}
|
|
178
609
|
async function resolveLocalghostContext(options = {}) {
|
|
179
610
|
const cwd = options.cwd ?? process.cwd();
|
|
180
|
-
const
|
|
611
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
612
|
+
cwd,
|
|
613
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
614
|
+
});
|
|
615
|
+
const merged = {
|
|
616
|
+
...projectConfig.config,
|
|
617
|
+
...defined(options)
|
|
618
|
+
};
|
|
619
|
+
const readOptions = readOptionsFromContext({ ...merged, cwd });
|
|
181
620
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
182
621
|
const configEntries = readDevHosts(readOptions);
|
|
183
|
-
const requestedPort =
|
|
184
|
-
const dynamicPort =
|
|
185
|
-
const bindHost =
|
|
622
|
+
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
623
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
624
|
+
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
186
625
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
187
626
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
188
|
-
const
|
|
627
|
+
const wwwAlias = merged.wwwAlias ?? true;
|
|
628
|
+
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
189
629
|
const hosts = uniqueHosts(entries);
|
|
190
|
-
const primaryHost =
|
|
630
|
+
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
631
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
632
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
633
|
+
route: getRouteName(primaryHost, projectName),
|
|
634
|
+
project: projectName,
|
|
635
|
+
owner: getLocalOwner(cwd)
|
|
636
|
+
});
|
|
191
637
|
return {
|
|
192
638
|
cwd,
|
|
193
|
-
projectName
|
|
639
|
+
projectName,
|
|
194
640
|
readOptions,
|
|
195
641
|
configPath: resolvedPath.path,
|
|
196
642
|
configFileName: resolvedPath.fileName,
|
|
@@ -202,7 +648,10 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
202
648
|
dynamicPort,
|
|
203
649
|
bindHost,
|
|
204
650
|
primaryHost,
|
|
205
|
-
https:
|
|
651
|
+
https: merged.https ?? envHttps() ?? false,
|
|
652
|
+
wwwAlias,
|
|
653
|
+
ghostTunnel,
|
|
654
|
+
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
206
655
|
};
|
|
207
656
|
}
|
|
208
657
|
|
|
@@ -241,21 +690,21 @@ function isProductionLike(env = process.env) {
|
|
|
241
690
|
}
|
|
242
691
|
|
|
243
692
|
// src/fs.ts
|
|
244
|
-
import { mkdirSync, readFileSync as
|
|
245
|
-
import { dirname } from "path";
|
|
693
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
694
|
+
import { dirname as dirname2 } from "path";
|
|
246
695
|
function readTextFile(path) {
|
|
247
|
-
return
|
|
696
|
+
return readFileSync4(path, "utf8");
|
|
248
697
|
}
|
|
249
698
|
function writeTextFile(path, value) {
|
|
250
|
-
|
|
251
|
-
|
|
699
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
700
|
+
writeFileSync2(path, value, "utf8");
|
|
252
701
|
return path;
|
|
253
702
|
}
|
|
254
703
|
|
|
255
704
|
// src/hosts-file.ts
|
|
256
|
-
import { writeFileSync as
|
|
705
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
257
706
|
import { tmpdir } from "os";
|
|
258
|
-
import { join as
|
|
707
|
+
import { join as join4 } from "path";
|
|
259
708
|
import { execa as execa2 } from "execa";
|
|
260
709
|
function escapeRegExp(value) {
|
|
261
710
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -290,8 +739,8 @@ ${block}`;
|
|
|
290
739
|
}
|
|
291
740
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
292
741
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
293
|
-
const tempPath =
|
|
294
|
-
|
|
742
|
+
const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
743
|
+
writeFileSync3(tempPath, next, "utf8");
|
|
295
744
|
if (process.platform === "win32") {
|
|
296
745
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
297
746
|
}
|
|
@@ -342,26 +791,26 @@ async function ask(question, defaultValue) {
|
|
|
342
791
|
}
|
|
343
792
|
|
|
344
793
|
// src/state.ts
|
|
345
|
-
import { existsSync as
|
|
346
|
-
import { join as
|
|
794
|
+
import { existsSync as existsSync4 } from "fs";
|
|
795
|
+
import { join as join5 } from "path";
|
|
347
796
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
348
797
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
349
|
-
return
|
|
798
|
+
return join5(cwd, LOCALGHOST_STATE_FILE);
|
|
350
799
|
}
|
|
351
800
|
function readLocalghostState(cwd = process.cwd()) {
|
|
352
801
|
const path = getLocalghostStatePath(cwd);
|
|
353
|
-
if (!
|
|
802
|
+
if (!existsSync4(path)) return null;
|
|
354
803
|
return JSON.parse(readTextFile(path));
|
|
355
804
|
}
|
|
356
805
|
function writeLocalghostState(cwd, state) {
|
|
357
806
|
const path = getLocalghostStatePath(cwd);
|
|
358
|
-
writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
807
|
+
writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
359
808
|
`);
|
|
360
809
|
return path;
|
|
361
810
|
}
|
|
362
811
|
|
|
363
812
|
// src/caddy.ts
|
|
364
|
-
import { dirname as
|
|
813
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
365
814
|
import { execa as execa3 } from "execa";
|
|
366
815
|
function groupByPort(entries) {
|
|
367
816
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -373,7 +822,7 @@ function groupByPort(entries) {
|
|
|
373
822
|
return groups;
|
|
374
823
|
}
|
|
375
824
|
function getCaddyfilePath(cwd = process.cwd()) {
|
|
376
|
-
return
|
|
825
|
+
return join6(cwd, "ops/local/Caddyfile");
|
|
377
826
|
}
|
|
378
827
|
function renderCaddyfile(entries, options = {}) {
|
|
379
828
|
const groups = groupByPort(entries);
|
|
@@ -399,11 +848,54 @@ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
|
|
|
399
848
|
}
|
|
400
849
|
async function validateCaddyfile(path) {
|
|
401
850
|
await execa3("caddy", ["validate", "--config", path], {
|
|
402
|
-
cwd:
|
|
851
|
+
cwd: dirname3(path),
|
|
403
852
|
stdio: "inherit"
|
|
404
853
|
});
|
|
405
854
|
}
|
|
406
855
|
|
|
856
|
+
// src/routes.ts
|
|
857
|
+
var ansi = {
|
|
858
|
+
cyan: "\x1B[36m",
|
|
859
|
+
dim: "\x1B[2m",
|
|
860
|
+
green: "\x1B[32m",
|
|
861
|
+
reset: "\x1B[0m",
|
|
862
|
+
yellow: "\x1B[33m"
|
|
863
|
+
};
|
|
864
|
+
function colorize(value, color, enabled) {
|
|
865
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
866
|
+
}
|
|
867
|
+
function colorizeUrl(value, enabled) {
|
|
868
|
+
if (!enabled) return value;
|
|
869
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
870
|
+
}
|
|
871
|
+
function formatGhostTunnel(config, options = {}) {
|
|
872
|
+
if (!config.enabled) return null;
|
|
873
|
+
const color = options.color === true;
|
|
874
|
+
const label = options.label ?? "expected";
|
|
875
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
876
|
+
const lines = [
|
|
877
|
+
"localghost ghost tunnel",
|
|
878
|
+
` mode: ${config.mode}`
|
|
879
|
+
];
|
|
880
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
881
|
+
if (urls.length === 0) {
|
|
882
|
+
lines.push(` ${label}: unavailable`);
|
|
883
|
+
} else if (urls.length === 1) {
|
|
884
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
885
|
+
} else {
|
|
886
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
887
|
+
for (const url of urls) {
|
|
888
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (options.verbose) {
|
|
892
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
893
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
894
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
895
|
+
}
|
|
896
|
+
return lines.join("\n");
|
|
897
|
+
}
|
|
898
|
+
|
|
407
899
|
// src/vite.ts
|
|
408
900
|
function mergeAllowedHosts(current, hosts) {
|
|
409
901
|
if (Array.isArray(current)) {
|
|
@@ -418,7 +910,11 @@ function getDisplayEntries(entries, vitePort) {
|
|
|
418
910
|
const matchingEntries = entries.filter((entry) => entry.port === vitePort);
|
|
419
911
|
return matchingEntries.length > 0 ? matchingEntries : entries;
|
|
420
912
|
}
|
|
421
|
-
function printLocalHosts(server,
|
|
913
|
+
function printLocalHosts(server, context) {
|
|
914
|
+
if (!context) return;
|
|
915
|
+
const entries = context.entries;
|
|
916
|
+
const vitePort = context.port;
|
|
917
|
+
const https = context.https;
|
|
422
918
|
const displayEntries = getDisplayEntries(entries, vitePort);
|
|
423
919
|
const protocol = https ? "https" : "http";
|
|
424
920
|
const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);
|
|
@@ -432,13 +928,24 @@ function printLocalHosts(server, entries, vitePort, https) {
|
|
|
432
928
|
` local: ${primaryUrl}`,
|
|
433
929
|
...urls.slice(1).map((url) => ` also: ${url}`),
|
|
434
930
|
vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
|
|
435
|
-
https ? " proxy: Caddy local HTTPS" : void 0
|
|
931
|
+
https ? " proxy: Caddy local HTTPS" : void 0,
|
|
932
|
+
context.ghostTunnel.enabled ? formatGhostTunnel(context.ghostTunnel, {
|
|
933
|
+
color: shouldColor(),
|
|
934
|
+
label: "ready",
|
|
935
|
+
verbose: optionsVerbose(context)
|
|
936
|
+
}) : void 0
|
|
436
937
|
].filter((line) => Boolean(line));
|
|
437
938
|
server.config.logger.info(lines.join("\n"), {
|
|
438
939
|
clear: false,
|
|
439
940
|
timestamp: false
|
|
440
941
|
});
|
|
441
942
|
}
|
|
943
|
+
function shouldColor() {
|
|
944
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
945
|
+
}
|
|
946
|
+
function optionsVerbose(context) {
|
|
947
|
+
return process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true" || context.ghostTunnel.displayUrls.length > 1;
|
|
948
|
+
}
|
|
442
949
|
function readOptionsFromPlugin(options) {
|
|
443
950
|
return {
|
|
444
951
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -452,7 +959,8 @@ function getConfigWatchFiles(options) {
|
|
|
452
959
|
const cwd = readOptions.cwd ?? process.cwd();
|
|
453
960
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
454
961
|
const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve2(cwd, fileName));
|
|
455
|
-
|
|
962
|
+
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));
|
|
963
|
+
return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path, ...projectConfigPaths])];
|
|
456
964
|
}
|
|
457
965
|
function normalizeWatchPath(filePath) {
|
|
458
966
|
return normalize(resolve2(filePath));
|
|
@@ -469,6 +977,57 @@ function defaultHost(cwd) {
|
|
|
469
977
|
const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
|
|
470
978
|
return `${projectName}.localhost`;
|
|
471
979
|
}
|
|
980
|
+
function getPackageOwner2(cwd) {
|
|
981
|
+
try {
|
|
982
|
+
const pkg = JSON.parse(readFileSync5(resolve2(cwd, "package.json"), "utf8"));
|
|
983
|
+
if (typeof pkg.name === "string" && pkg.name.startsWith("@")) {
|
|
984
|
+
return pkg.name.slice(1).split("/")[0];
|
|
985
|
+
}
|
|
986
|
+
} catch {
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
function getLocalOwner2(cwd) {
|
|
992
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner2(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
993
|
+
}
|
|
994
|
+
function getRouteName2(primaryHost, fallback) {
|
|
995
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
996
|
+
}
|
|
997
|
+
function readBuildEntries(options) {
|
|
998
|
+
const readOptions = readOptionsFromPlugin(options);
|
|
999
|
+
const resolved = resolveDevHostsPath(readOptions);
|
|
1000
|
+
if (!resolved.exists) return [];
|
|
1001
|
+
try {
|
|
1002
|
+
return readDevHosts(readOptions);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function maybePrintBuildGhostTunnel(options) {
|
|
1008
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1009
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
1010
|
+
cwd,
|
|
1011
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
1012
|
+
});
|
|
1013
|
+
const explicitGhostTunnel = typeof options.ghostTunnel !== "undefined" ? options.ghostTunnel : projectConfig.config.ghostTunnel;
|
|
1014
|
+
if (!explicitGhostTunnel) return;
|
|
1015
|
+
const entries = readBuildEntries(options);
|
|
1016
|
+
const projectName = sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd));
|
|
1017
|
+
const primaryHost = options.primaryHost ?? entries[0]?.host ?? defaultHost(cwd);
|
|
1018
|
+
const ghostTunnel = resolveGhostTunnelConfig(explicitGhostTunnel, {
|
|
1019
|
+
route: getRouteName2(primaryHost, projectName),
|
|
1020
|
+
project: sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd)),
|
|
1021
|
+
owner: getLocalOwner2(cwd)
|
|
1022
|
+
});
|
|
1023
|
+
if (!ghostTunnel.enabled) return;
|
|
1024
|
+
const formatted = formatGhostTunnel(ghostTunnel, {
|
|
1025
|
+
color: shouldColor(),
|
|
1026
|
+
label: "configured",
|
|
1027
|
+
verbose: options.verbose === true || process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true"
|
|
1028
|
+
});
|
|
1029
|
+
if (formatted) console.log(formatted);
|
|
1030
|
+
}
|
|
472
1031
|
async function promptForHosts(cwd, port) {
|
|
473
1032
|
const primaryHost = await ask("Primary local domain", defaultHost(cwd));
|
|
474
1033
|
const hosts = [primaryHost.toLowerCase()];
|
|
@@ -476,20 +1035,20 @@ async function promptForHosts(cwd, port) {
|
|
|
476
1035
|
const host = await ask("Domain");
|
|
477
1036
|
if (host) hosts.push(host.toLowerCase());
|
|
478
1037
|
}
|
|
479
|
-
return [...new Set(hosts)];
|
|
1038
|
+
return [...new Set(addDefaultWwwAliases(hosts.map((host) => ({ host, port, target: `127.0.0.1:${port}` }))).map((entry) => entry.host))];
|
|
480
1039
|
}
|
|
481
1040
|
function hasReadySetup(cwd, entries, configPath, https) {
|
|
482
1041
|
const state = readLocalghostState(cwd);
|
|
483
1042
|
const projectName = sanitizeProjectName(getProjectName(cwd));
|
|
484
1043
|
if (state?.action !== "setup" || state.configPath !== configPath) return false;
|
|
485
1044
|
try {
|
|
486
|
-
const hosts =
|
|
1045
|
+
const hosts = readFileSync5(getSystemHostsPath(), "utf8");
|
|
487
1046
|
if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
|
|
488
1047
|
} catch {
|
|
489
1048
|
return false;
|
|
490
1049
|
}
|
|
491
1050
|
const caddyfilePath = getCaddyfilePath(cwd);
|
|
492
|
-
return
|
|
1051
|
+
return existsSync5(caddyfilePath) && readFileSync5(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
|
|
493
1052
|
}
|
|
494
1053
|
async function setupProject(cwd, entries, configPath, https) {
|
|
495
1054
|
const caddy = await checkCaddy();
|
|
@@ -518,6 +1077,14 @@ async function setupProject(cwd, entries, configPath, https) {
|
|
|
518
1077
|
caddyHttps: https,
|
|
519
1078
|
entries
|
|
520
1079
|
});
|
|
1080
|
+
registerLocalghostSetup({
|
|
1081
|
+
cwd,
|
|
1082
|
+
projectName,
|
|
1083
|
+
configPath,
|
|
1084
|
+
caddyfilePath,
|
|
1085
|
+
https,
|
|
1086
|
+
entries
|
|
1087
|
+
});
|
|
521
1088
|
}
|
|
522
1089
|
async function ensureLocalghostContext(options, vitePort, https) {
|
|
523
1090
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -541,38 +1108,116 @@ async function ensureLocalghostContext(options, vitePort, https) {
|
|
|
541
1108
|
...options,
|
|
542
1109
|
cwd,
|
|
543
1110
|
port: vitePort,
|
|
544
|
-
https
|
|
1111
|
+
...typeof https === "boolean" ? { https } : {}
|
|
545
1112
|
});
|
|
546
|
-
if (!hasReadySetup(cwd, context.entries, resolved.path, https)) {
|
|
1113
|
+
if (!hasReadySetup(cwd, context.entries, resolved.path, context.https)) {
|
|
547
1114
|
if (options.setup === false || !canPrompt()) return context;
|
|
548
1115
|
const setup = await confirm("Run caddy:setup now?", true);
|
|
549
1116
|
if (setup) {
|
|
550
|
-
await setupProject(cwd, context.entries, resolved.path, https);
|
|
1117
|
+
await setupProject(cwd, context.entries, resolved.path, context.https);
|
|
551
1118
|
console.log(`All set. Setup state: ${getLocalghostStatePath(cwd)}`);
|
|
552
1119
|
}
|
|
553
1120
|
}
|
|
554
1121
|
return context;
|
|
555
1122
|
}
|
|
1123
|
+
function isConcreteGhostUrl(url) {
|
|
1124
|
+
return !url.includes("*") && !url.includes("<") && /^https?:\/\//.test(url);
|
|
1125
|
+
}
|
|
1126
|
+
function getConcreteGhostUrls(context) {
|
|
1127
|
+
return context.ghostTunnel.displayUrls.filter(isConcreteGhostUrl);
|
|
1128
|
+
}
|
|
1129
|
+
function openExternalUrl(url) {
|
|
1130
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1131
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1132
|
+
const child = spawn(command, args, {
|
|
1133
|
+
detached: true,
|
|
1134
|
+
stdio: "ignore"
|
|
1135
|
+
});
|
|
1136
|
+
child.unref();
|
|
1137
|
+
}
|
|
1138
|
+
function installGhostTunnelMenu(server, context) {
|
|
1139
|
+
if (!context?.ghostTunnel.enabled || !process.stdin.isTTY) return void 0;
|
|
1140
|
+
emitKeypressEvents(process.stdin);
|
|
1141
|
+
let active = false;
|
|
1142
|
+
const concreteUrls = getConcreteGhostUrls(context);
|
|
1143
|
+
const printMenu = () => {
|
|
1144
|
+
active = true;
|
|
1145
|
+
const lines = [
|
|
1146
|
+
"",
|
|
1147
|
+
formatGhostTunnel(context.ghostTunnel, {
|
|
1148
|
+
color: shouldColor(),
|
|
1149
|
+
label: "ready",
|
|
1150
|
+
verbose: true
|
|
1151
|
+
}) ?? "localghost ghost tunnel",
|
|
1152
|
+
""
|
|
1153
|
+
];
|
|
1154
|
+
if (concreteUrls.length === 0) {
|
|
1155
|
+
lines.push(" No concrete Ghost Tunnel domain configured.");
|
|
1156
|
+
lines.push(" Add ghostTunnel.domains to localghost.config.mjs to open a URL from this menu.");
|
|
1157
|
+
active = false;
|
|
1158
|
+
} else {
|
|
1159
|
+
concreteUrls.forEach((url, index) => {
|
|
1160
|
+
lines.push(` ${index + 1}. ${url}`);
|
|
1161
|
+
});
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push(" Press a number to open, or escape to cancel.");
|
|
1164
|
+
}
|
|
1165
|
+
server.config.logger.info(lines.join("\n"), {
|
|
1166
|
+
clear: false,
|
|
1167
|
+
timestamp: false
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
const onKeypress = (_input, key = {}) => {
|
|
1171
|
+
if (key.ctrl && key.name === "c") return;
|
|
1172
|
+
if (!active) {
|
|
1173
|
+
if (key.name === "g") printMenu();
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (key.name === "escape") {
|
|
1177
|
+
active = false;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const index = Number.parseInt(key.name ?? "", 10) - 1;
|
|
1181
|
+
const url = concreteUrls[index];
|
|
1182
|
+
if (!url) return;
|
|
1183
|
+
active = false;
|
|
1184
|
+
openExternalUrl(url);
|
|
1185
|
+
server.config.logger.info(`localghost opened ${url}`, {
|
|
1186
|
+
clear: false,
|
|
1187
|
+
timestamp: false
|
|
1188
|
+
});
|
|
1189
|
+
};
|
|
1190
|
+
process.stdin.on("keypress", onKeypress);
|
|
1191
|
+
return () => {
|
|
1192
|
+
process.stdin.off("keypress", onKeypress);
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
556
1195
|
function localGhostPlugin(options = {}) {
|
|
557
1196
|
let resolvedEntries = [];
|
|
558
1197
|
let resolvedVitePort;
|
|
1198
|
+
let resolvedHttps = false;
|
|
1199
|
+
let resolvedContext;
|
|
559
1200
|
let restartTimer;
|
|
1201
|
+
let activityRunId;
|
|
560
1202
|
return {
|
|
561
1203
|
name: "localghost:vite",
|
|
562
1204
|
enforce: "pre",
|
|
563
1205
|
async config(userConfig, configEnv) {
|
|
564
1206
|
if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
|
|
1207
|
+
await maybePrintBuildGhostTunnel(options);
|
|
565
1208
|
return {};
|
|
566
1209
|
}
|
|
567
1210
|
const existingServer = userConfig.server ?? {};
|
|
568
1211
|
const envVitePort = Number.parseInt(process.env.LOCALGHOST_PORT ?? process.env.VITE_PORT ?? "", 10);
|
|
569
1212
|
const requestedVitePort = options.port ?? existingServer.port ?? (Number.isInteger(envVitePort) ? envVitePort : 5173);
|
|
570
|
-
const context = await ensureLocalghostContext(options, requestedVitePort,
|
|
1213
|
+
const context = await ensureLocalghostContext(options, requestedVitePort, options.https);
|
|
571
1214
|
const entries = context.entries;
|
|
572
1215
|
const hosts = context.hosts;
|
|
573
1216
|
const primaryHost = context.primaryHost;
|
|
574
1217
|
resolvedEntries = entries;
|
|
575
1218
|
resolvedVitePort = context.port;
|
|
1219
|
+
resolvedHttps = context.https;
|
|
1220
|
+
resolvedContext = context;
|
|
576
1221
|
const server = {
|
|
577
1222
|
...existingServer,
|
|
578
1223
|
allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
|
|
@@ -584,7 +1229,7 @@ function localGhostPlugin(options = {}) {
|
|
|
584
1229
|
if (context.port) {
|
|
585
1230
|
server.port = context.port;
|
|
586
1231
|
}
|
|
587
|
-
if (
|
|
1232
|
+
if (context.https && primaryHost) {
|
|
588
1233
|
const existingWs = typeof server.ws === "object" && server.ws ? server.ws : {};
|
|
589
1234
|
const existingHmr = typeof existingServer.hmr === "object" && existingServer.hmr ? existingServer.hmr : {};
|
|
590
1235
|
server.ws = {
|
|
@@ -632,9 +1277,38 @@ function localGhostPlugin(options = {}) {
|
|
|
632
1277
|
server.watcher.on("unlink", restartOnLocalghostConfigChange);
|
|
633
1278
|
if (options.log !== false) {
|
|
634
1279
|
server.printUrls = () => {
|
|
635
|
-
printLocalHosts(server,
|
|
1280
|
+
printLocalHosts(server, resolvedContext);
|
|
636
1281
|
};
|
|
637
1282
|
}
|
|
1283
|
+
if (resolvedContext) {
|
|
1284
|
+
const run = registerLocalghostRun({
|
|
1285
|
+
id: `${resolvedContext.projectName}:vite:${process.pid}:${resolvedContext.cwd}`,
|
|
1286
|
+
mode: "vite",
|
|
1287
|
+
pid: process.pid,
|
|
1288
|
+
cwd: resolvedContext.cwd,
|
|
1289
|
+
projectName: resolvedContext.projectName,
|
|
1290
|
+
configPath: resolvedContext.configPath,
|
|
1291
|
+
childCommand: ["vite"],
|
|
1292
|
+
https: resolvedContext.https,
|
|
1293
|
+
requestedPort: resolvedContext.requestedPort,
|
|
1294
|
+
port: resolvedContext.port,
|
|
1295
|
+
dynamicPort: resolvedContext.dynamicPort,
|
|
1296
|
+
entries: resolvedContext.entries
|
|
1297
|
+
});
|
|
1298
|
+
activityRunId = run.id;
|
|
1299
|
+
}
|
|
1300
|
+
const cleanupGhostMenu = installGhostTunnelMenu(server, resolvedContext);
|
|
1301
|
+
const cleanupActivity = () => {
|
|
1302
|
+
if (!activityRunId) return;
|
|
1303
|
+
unregisterLocalghostRun(activityRunId);
|
|
1304
|
+
activityRunId = void 0;
|
|
1305
|
+
};
|
|
1306
|
+
const cleanup = () => {
|
|
1307
|
+
cleanupActivity();
|
|
1308
|
+
cleanupGhostMenu?.();
|
|
1309
|
+
};
|
|
1310
|
+
server.httpServer?.once("close", cleanup);
|
|
1311
|
+
process.once("exit", cleanup);
|
|
638
1312
|
}
|
|
639
1313
|
};
|
|
640
1314
|
}
|