@hamedb89/localghost 0.1.13 → 0.1.15
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 +18 -2
- package/dist/cli.js +495 -126
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +80 -2
- package/dist/index.js +318 -36
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +218 -19
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +3 -1
- package/docs/ghost-tunnel.md +4 -1
- package/docs/localghost.1.md +29 -2
- package/package.json +1 -1
package/dist/vite.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ type LocalGhostPluginOptions = {
|
|
|
17
17
|
localghostConfig?: string | false;
|
|
18
18
|
wwwAlias?: boolean;
|
|
19
19
|
ghostTunnel?: GhostTunnelOptions;
|
|
20
|
+
registryOwnerToken?: string;
|
|
20
21
|
verbose?: boolean;
|
|
21
22
|
};
|
|
22
23
|
declare function localGhostPlugin(options?: LocalGhostPluginOptions): Plugin;
|
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/vite.ts
|
|
2
2
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
3
|
-
import { normalize, resolve as
|
|
3
|
+
import { normalize as normalize2, resolve as resolve3 } from "path";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { emitKeypressEvents } from "readline";
|
|
6
6
|
|
|
@@ -229,25 +229,30 @@ function getProjectName(cwd = process.cwd()) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
function sanitizeProjectName(value) {
|
|
232
|
-
const
|
|
232
|
+
const sanitized = value.replace(/[^\w.-]+/g, "-");
|
|
233
|
+
let start = 0;
|
|
234
|
+
let end = sanitized.length;
|
|
235
|
+
while (start < end && sanitized.charCodeAt(start) === 45) start += 1;
|
|
236
|
+
while (end > start && sanitized.charCodeAt(end - 1) === 45) end -= 1;
|
|
237
|
+
const projectName = sanitized.slice(start, end);
|
|
233
238
|
return projectName || "app";
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
// src/context.ts
|
|
237
242
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
238
|
-
import { join as
|
|
243
|
+
import { join as join4 } from "path";
|
|
239
244
|
import { pathToFileURL } from "url";
|
|
240
245
|
|
|
241
246
|
// src/port.ts
|
|
242
247
|
import { createServer } from "net";
|
|
243
248
|
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
244
|
-
return new Promise((
|
|
249
|
+
return new Promise((resolve4) => {
|
|
245
250
|
const server = createServer();
|
|
246
251
|
server.once("error", () => {
|
|
247
|
-
|
|
252
|
+
resolve4(false);
|
|
248
253
|
});
|
|
249
254
|
server.once("listening", () => {
|
|
250
|
-
server.close(() =>
|
|
255
|
+
server.close(() => resolve4(true));
|
|
251
256
|
});
|
|
252
257
|
server.listen(port, host);
|
|
253
258
|
});
|
|
@@ -264,6 +269,176 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
264
269
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
265
270
|
}
|
|
266
271
|
|
|
272
|
+
// src/registry.ts
|
|
273
|
+
import { randomUUID } from "crypto";
|
|
274
|
+
import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
|
|
275
|
+
import { homedir as homedir2 } from "os";
|
|
276
|
+
import { join as join3, normalize, resolve as resolve2 } from "path";
|
|
277
|
+
var LOCALGHOST_REGISTRY_FILE = "registry.json";
|
|
278
|
+
var LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
|
|
279
|
+
function defaultProcessRunning(pid) {
|
|
280
|
+
if (pid <= 0) return false;
|
|
281
|
+
try {
|
|
282
|
+
process.kill(pid, 0);
|
|
283
|
+
return true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return error.code === "EPERM";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function getLocalghostRegistryRoot(env = process.env) {
|
|
289
|
+
return resolve2(env.LOCALGHOST_HOME || join3(homedir2(), ".localghost"));
|
|
290
|
+
}
|
|
291
|
+
function canonicalizeLocalghostProjectCwd(cwd = process.cwd()) {
|
|
292
|
+
return normalize(resolve2(cwd));
|
|
293
|
+
}
|
|
294
|
+
function emptyRegistry() {
|
|
295
|
+
return { version: 1, allocations: [], leases: [] };
|
|
296
|
+
}
|
|
297
|
+
function leaseKey(projectCwd, instanceKey) {
|
|
298
|
+
return `${projectCwd}\0${instanceKey}`;
|
|
299
|
+
}
|
|
300
|
+
function validRegistry(value) {
|
|
301
|
+
if (!value || typeof value !== "object") return false;
|
|
302
|
+
const candidate = value;
|
|
303
|
+
return candidate.version === 1 && Array.isArray(candidate.allocations) && Array.isArray(candidate.leases);
|
|
304
|
+
}
|
|
305
|
+
function pruneRegistry(registry, now, isRunning) {
|
|
306
|
+
registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
|
|
307
|
+
}
|
|
308
|
+
async function readJson(path) {
|
|
309
|
+
try {
|
|
310
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error.code === "ENOENT") return void 0;
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function createLocalghostRegistry(options = {}) {
|
|
317
|
+
const root = resolve2(options.stateRoot ?? getLocalghostRegistryRoot());
|
|
318
|
+
const registryPath = join3(root, LOCALGHOST_REGISTRY_FILE);
|
|
319
|
+
const lockPath = join3(root, LOCALGHOST_REGISTRY_LOCK_FILE);
|
|
320
|
+
const cwd = canonicalizeLocalghostProjectCwd(options.cwd);
|
|
321
|
+
const now = options.now ?? Date.now;
|
|
322
|
+
const pid = options.pid ?? process.pid;
|
|
323
|
+
const ownerToken = options.ownerToken ?? randomUUID();
|
|
324
|
+
const isRunning = options.isProcessRunning ?? defaultProcessRunning;
|
|
325
|
+
const availabilityCheck = options.availabilityCheck ?? isPortAvailable;
|
|
326
|
+
const lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
|
|
327
|
+
const lockRetryMs = options.lockRetryMs ?? 25;
|
|
328
|
+
const lockStaleMs = options.lockStaleMs ?? 3e4;
|
|
329
|
+
async function readRegistry() {
|
|
330
|
+
const value = await readJson(registryPath);
|
|
331
|
+
return validRegistry(value) ? value : emptyRegistry();
|
|
332
|
+
}
|
|
333
|
+
async function writeRegistry(registry) {
|
|
334
|
+
await mkdir(root, { recursive: true });
|
|
335
|
+
const temporaryPath = join3(root, `.registry.${process.pid}.${randomUUID()}.tmp`);
|
|
336
|
+
await writeFile(temporaryPath, `${JSON.stringify(registry, null, 2)}
|
|
337
|
+
`, { mode: 384 });
|
|
338
|
+
await rename(temporaryPath, registryPath);
|
|
339
|
+
}
|
|
340
|
+
async function lock() {
|
|
341
|
+
await mkdir(root, { recursive: true });
|
|
342
|
+
const deadline = now() + lockTimeoutMs;
|
|
343
|
+
const token = randomUUID();
|
|
344
|
+
while (true) {
|
|
345
|
+
try {
|
|
346
|
+
const handle = await open(lockPath, "wx", 384);
|
|
347
|
+
await handle.writeFile(`${JSON.stringify({ pid, createdAt: now(), token })}
|
|
348
|
+
`);
|
|
349
|
+
await handle.close();
|
|
350
|
+
return async () => {
|
|
351
|
+
const current = await readJson(lockPath);
|
|
352
|
+
if (current?.token === token) await unlink(lockPath).catch(() => void 0);
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error.code !== "EEXIST") throw error;
|
|
356
|
+
const lockInfo = await readJson(lockPath);
|
|
357
|
+
let stale = false;
|
|
358
|
+
if (lockInfo && typeof lockInfo.pid === "number") {
|
|
359
|
+
stale = !isRunning(lockInfo.pid) && now() - lockInfo.createdAt >= 0;
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
stale = now() - (await stat(lockPath)).mtimeMs > lockStaleMs;
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (stale) {
|
|
368
|
+
await rm(lockPath, { force: true }).catch(() => void 0);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (now() >= deadline) throw new Error(`Timed out waiting for Localghost registry lock: ${lockPath}`);
|
|
372
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, lockRetryMs));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function withLock(operation) {
|
|
377
|
+
const releaseLock = await lock();
|
|
378
|
+
try {
|
|
379
|
+
const registry = await readRegistry();
|
|
380
|
+
pruneRegistry(registry, now(), isRunning);
|
|
381
|
+
return await operation(registry);
|
|
382
|
+
} finally {
|
|
383
|
+
await releaseLock();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
root,
|
|
388
|
+
registryPath,
|
|
389
|
+
lockPath,
|
|
390
|
+
ownerToken,
|
|
391
|
+
read: readRegistry,
|
|
392
|
+
async acquirePort(acquireOptions) {
|
|
393
|
+
const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
|
|
394
|
+
if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
|
|
395
|
+
return withLock(async (registry) => {
|
|
396
|
+
const key = leaseKey(projectCwd, acquireOptions.instanceKey);
|
|
397
|
+
const existing = registry.allocations.find((entry2) => leaseKey(entry2.projectCwd, entry2.instanceKey) === key);
|
|
398
|
+
const reserved = new Set(acquireOptions.reservedPorts ?? []);
|
|
399
|
+
const activePorts = new Set(registry.leases.map((lease2) => lease2.port));
|
|
400
|
+
const port = existing?.port;
|
|
401
|
+
const ownsActiveLease = registry.leases.some((lease2) => lease2.port === port && leaseKey(lease2.projectCwd, lease2.instanceKey) === key && lease2.ownerToken === ownerToken);
|
|
402
|
+
const reusable = port !== void 0 && !reserved.has(port) && (!activePorts.has(port) || ownsActiveLease) && (ownsActiveLease || await availabilityCheck(port, acquireOptions.host));
|
|
403
|
+
let selectedPort = reusable ? port : void 0;
|
|
404
|
+
if (selectedPort === void 0) {
|
|
405
|
+
const startPort = acquireOptions.startPort ?? 3e3;
|
|
406
|
+
const maxAttempts = acquireOptions.maxAttempts ?? 50;
|
|
407
|
+
for (let offset = 0; offset < maxAttempts; offset += 1) {
|
|
408
|
+
const candidate = startPort + offset;
|
|
409
|
+
if (reserved.has(candidate) || activePorts.has(candidate)) continue;
|
|
410
|
+
if (await availabilityCheck(candidate, acquireOptions.host)) {
|
|
411
|
+
selectedPort = candidate;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (selectedPort === void 0) throw new Error(`No available registry port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
416
|
+
}
|
|
417
|
+
const timestamp = now();
|
|
418
|
+
const entry = existing ?? { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, updatedAt: timestamp };
|
|
419
|
+
entry.port = selectedPort;
|
|
420
|
+
entry.updatedAt = timestamp;
|
|
421
|
+
if (!existing) registry.allocations.push(entry);
|
|
422
|
+
registry.leases = registry.leases.filter((lease2) => leaseKey(lease2.projectCwd, lease2.instanceKey) !== key);
|
|
423
|
+
const lease = { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, pid, acquiredAt: timestamp, expiresAt: timestamp + (acquireOptions.leaseTtlMs ?? 30 * 60 * 1e3), ownerToken };
|
|
424
|
+
registry.leases.push(lease);
|
|
425
|
+
await writeRegistry(registry);
|
|
426
|
+
return lease;
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
async releasePort(releaseOptions) {
|
|
430
|
+
const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
|
|
431
|
+
return withLock(async (registry) => {
|
|
432
|
+
const key = leaseKey(projectCwd, releaseOptions.instanceKey);
|
|
433
|
+
const before = registry.leases.length;
|
|
434
|
+
registry.leases = registry.leases.filter((lease) => leaseKey(lease.projectCwd, lease.instanceKey) !== key || lease.ownerToken !== ownerToken);
|
|
435
|
+
if (registry.leases.length !== before) await writeRegistry(registry);
|
|
436
|
+
return registry.leases.length !== before;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
267
442
|
// src/tunnel.ts
|
|
268
443
|
import { domainToASCII } from "url";
|
|
269
444
|
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
@@ -641,7 +816,7 @@ function envHttps() {
|
|
|
641
816
|
}
|
|
642
817
|
function getPackageName(cwd) {
|
|
643
818
|
try {
|
|
644
|
-
const pkg = JSON.parse(readFileSync3(
|
|
819
|
+
const pkg = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf8"));
|
|
645
820
|
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
646
821
|
} catch {
|
|
647
822
|
return void 0;
|
|
@@ -724,7 +899,24 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
724
899
|
const autoRepair = merged.autoRepair ?? true;
|
|
725
900
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
726
901
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
727
|
-
|
|
902
|
+
let port = requestedPort;
|
|
903
|
+
let releasePort;
|
|
904
|
+
const reservePort = merged.reservePort ?? false;
|
|
905
|
+
const instanceKey = merged.instanceKey ?? "run";
|
|
906
|
+
if (reservePort && dynamicPort) {
|
|
907
|
+
const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
|
|
908
|
+
const lease = await registry.acquirePort({
|
|
909
|
+
projectCwd: cwd,
|
|
910
|
+
instanceKey,
|
|
911
|
+
startPort: requestedPort,
|
|
912
|
+
host: probeHost,
|
|
913
|
+
...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
|
|
914
|
+
});
|
|
915
|
+
port = lease.port;
|
|
916
|
+
releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
|
|
917
|
+
} else if (dynamicPort) {
|
|
918
|
+
port = await findAvailablePort(requestedPort, { host: probeHost });
|
|
919
|
+
}
|
|
728
920
|
const wwwAlias = merged.wwwAlias ?? true;
|
|
729
921
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
730
922
|
const hosts = uniqueHosts(entries);
|
|
@@ -753,7 +945,8 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
753
945
|
https: merged.https ?? envHttps() ?? false,
|
|
754
946
|
wwwAlias,
|
|
755
947
|
ghostTunnel,
|
|
756
|
-
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
948
|
+
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
|
|
949
|
+
...releasePort ? { releasePort } : {}
|
|
757
950
|
};
|
|
758
951
|
}
|
|
759
952
|
|
|
@@ -806,7 +999,7 @@ function writeTextFile(path, value) {
|
|
|
806
999
|
// src/hosts-file.ts
|
|
807
1000
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
808
1001
|
import { tmpdir } from "os";
|
|
809
|
-
import { join as
|
|
1002
|
+
import { join as join5 } from "path";
|
|
810
1003
|
import { execa as execa2 } from "execa";
|
|
811
1004
|
function escapeRegExp(value) {
|
|
812
1005
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -842,7 +1035,7 @@ ${block}`;
|
|
|
842
1035
|
}
|
|
843
1036
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
844
1037
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
845
|
-
const tempPath =
|
|
1038
|
+
const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
846
1039
|
writeFileSync3(tempPath, next, "utf8");
|
|
847
1040
|
if (process.env.LOCALGHOST_HOSTS_PATH) {
|
|
848
1041
|
writeFileSync3(hostsPath, next, "utf8");
|
|
@@ -899,10 +1092,10 @@ async function ask(question, defaultValue) {
|
|
|
899
1092
|
|
|
900
1093
|
// src/state.ts
|
|
901
1094
|
import { existsSync as existsSync4 } from "fs";
|
|
902
|
-
import { join as
|
|
1095
|
+
import { join as join6 } from "path";
|
|
903
1096
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
904
1097
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
905
|
-
return
|
|
1098
|
+
return join6(cwd, LOCALGHOST_STATE_FILE);
|
|
906
1099
|
}
|
|
907
1100
|
function readLocalghostState(cwd = process.cwd()) {
|
|
908
1101
|
const path = getLocalghostStatePath(cwd);
|
|
@@ -917,7 +1110,7 @@ function writeLocalghostState(cwd, state) {
|
|
|
917
1110
|
}
|
|
918
1111
|
|
|
919
1112
|
// src/caddy.ts
|
|
920
|
-
import { dirname as dirname3, join as
|
|
1113
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
921
1114
|
import { execa as execa3 } from "execa";
|
|
922
1115
|
function shouldShowCaddyLogs() {
|
|
923
1116
|
return ["1", "true", "yes", "on"].includes((process.env.LOCALGHOST_CADDY_VERBOSE ?? "").toLowerCase());
|
|
@@ -935,7 +1128,7 @@ function groupByPort(entries) {
|
|
|
935
1128
|
return groups;
|
|
936
1129
|
}
|
|
937
1130
|
function getCaddyfilePath(cwd = process.cwd()) {
|
|
938
|
-
return
|
|
1131
|
+
return join7(cwd, "ops/local/Caddyfile");
|
|
939
1132
|
}
|
|
940
1133
|
function renderCaddyfile(entries, options = {}) {
|
|
941
1134
|
const groups = groupByPort(entries);
|
|
@@ -1073,12 +1266,12 @@ function getConfigWatchFiles(options) {
|
|
|
1073
1266
|
const readOptions = readOptionsFromPlugin(options);
|
|
1074
1267
|
const cwd = readOptions.cwd ?? process.cwd();
|
|
1075
1268
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
1076
|
-
const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) =>
|
|
1077
|
-
const projectConfigPaths = options.localghostConfig === false ? [] : options.localghostConfig ? [
|
|
1269
|
+
const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve3(cwd, fileName));
|
|
1270
|
+
const projectConfigPaths = options.localghostConfig === false ? [] : options.localghostConfig ? [resolve3(cwd, options.localghostConfig)] : ["localghost.config.mjs", "localghost.config.js", "localghost.config.cjs"].map((fileName) => resolve3(cwd, fileName));
|
|
1078
1271
|
return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path, ...projectConfigPaths])];
|
|
1079
1272
|
}
|
|
1080
1273
|
function normalizeWatchPath(filePath) {
|
|
1081
|
-
return
|
|
1274
|
+
return normalize2(resolve3(filePath));
|
|
1082
1275
|
}
|
|
1083
1276
|
function renderConfig(hosts, port) {
|
|
1084
1277
|
return [
|
|
@@ -1094,7 +1287,7 @@ function defaultHost(cwd) {
|
|
|
1094
1287
|
}
|
|
1095
1288
|
function getPackageOwner2(cwd) {
|
|
1096
1289
|
try {
|
|
1097
|
-
const pkg = JSON.parse(readFileSync5(
|
|
1290
|
+
const pkg = JSON.parse(readFileSync5(resolve3(cwd, "package.json"), "utf8"));
|
|
1098
1291
|
if (typeof pkg.name === "string" && pkg.name.startsWith("@")) {
|
|
1099
1292
|
return pkg.name.slice(1).split("/")[0];
|
|
1100
1293
|
}
|
|
@@ -1219,10 +1412,15 @@ async function ensureLocalghostContext(options, vitePort, https) {
|
|
|
1219
1412
|
writeTextFile(resolved.path, renderConfig(hosts, vitePort));
|
|
1220
1413
|
console.log(`Created ${resolved.path}`);
|
|
1221
1414
|
}
|
|
1415
|
+
const wrapperManagedPort = Boolean(process.env.LOCALGHOST_PORT);
|
|
1222
1416
|
const context = await resolveLocalghostContext({
|
|
1223
1417
|
...options,
|
|
1224
1418
|
cwd,
|
|
1225
1419
|
port: vitePort,
|
|
1420
|
+
reservePort: !wrapperManagedPort,
|
|
1421
|
+
instanceKey: "vite",
|
|
1422
|
+
registryOwnerToken: options.registryOwnerToken ?? `${process.pid}:vite:${cwd}`,
|
|
1423
|
+
...wrapperManagedPort ? { dynamicPort: false } : {},
|
|
1226
1424
|
...typeof https === "boolean" ? { https } : {}
|
|
1227
1425
|
});
|
|
1228
1426
|
if (!hasReadySetup(cwd, context.entries, resolved.path, context.https)) {
|
|
@@ -1420,6 +1618,7 @@ function localGhostPlugin(options = {}) {
|
|
|
1420
1618
|
const cleanup = () => {
|
|
1421
1619
|
cleanupActivity();
|
|
1422
1620
|
cleanupGhostMenu?.();
|
|
1621
|
+
void resolvedContext?.releasePort?.();
|
|
1423
1622
|
};
|
|
1424
1623
|
server.httpServer?.once("close", cleanup);
|
|
1425
1624
|
process.once("exit", cleanup);
|