From 963d5d83c4859b336998f2d0c426c4bc0e7f459f Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sat, 16 May 2026 08:13:11 +0200 Subject: [PATCH] feat: auto-shift port when default is in use (Vite-style) If the requested port is busy, probe up to 10 ports above it (net.createServer-based bind test on the same host JSS will use) and use the first free one. Print a clear "Port 5444 is in use, using 5445 instead." line if shifted. Errors with a hint to pass --port if no slot is free in the range. Mirrors Vite/Next.js dev-server behaviour. Solves the most common "jspod already running" friction without forcing the user to read an error and retry with --port. Bumps jspod to 0.0.16. --- index.js | 30 ++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index bf7cf42..65e075f 100755 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ import { dirname, join, delimiter } from 'path'; import chalk from 'chalk'; import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync, copyFileSync } from 'fs'; import { randomBytes } from 'crypto'; +import { createServer } from 'net'; const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')); @@ -161,6 +162,35 @@ if (!existsSync(options.root)) { mkdirSync(options.root, { recursive: true }); } +// Find a free port starting at the requested one. Mirrors Vite's +// behaviour: shift up by one and try again, up to 10 attempts. We probe +// by binding a throwaway server on the same host the spawned JSS will +// use, so the result reflects the actual interface we'll claim. +async function findFreePort(startPort, host, maxTries = 10) { + for (let p = startPort; p < startPort + maxTries; p++) { + const free = await new Promise((resolve) => { + const srv = createServer(); + srv.once('error', () => resolve(false)); + srv.once('listening', () => srv.close(() => resolve(true))); + srv.listen(p, host); + }); + if (free) return p; + } + return null; +} + +const requestedPort = options.port; +const freePort = await findFreePort(requestedPort, options.host); +if (freePort === null) { + console.error(chalk.red(`✗ No free port in range ${requestedPort}-${requestedPort + 9} on ${options.host}.`)); + console.error(chalk.dim('Pass --port to pick a different starting port.')); + process.exit(1); +} +if (freePort !== requestedPort) { + console.log(chalk.yellow(`Port ${requestedPort} is in use, using ${freePort} instead.`)); +} +options.port = freePort; + // Resolve the JWT signing secret. Priority: // 1. TOKEN_SECRET env var (operator-controlled) // 2. Persisted random secret at /.token-secret (generated on diff --git a/package.json b/package.json index 371c699..21f4e49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jspod", - "version": "0.0.15", + "version": "0.0.16", "description": "JavaScript Solid Pod - Just works, batteries included", "type": "module", "main": "index.js",