Skip to content

Commit eff3fc9

Browse files
feat: auto-shift port when default is in use (Vite-style) (#14)
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.
1 parent 7333638 commit eff3fc9

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { dirname, join, delimiter } from 'path';
1111
import chalk from 'chalk';
1212
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync, copyFileSync } from 'fs';
1313
import { randomBytes } from 'crypto';
14+
import { createServer } from 'net';
1415

1516
const __dirname = dirname(fileURLToPath(import.meta.url));
1617
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
@@ -161,6 +162,35 @@ if (!existsSync(options.root)) {
161162
mkdirSync(options.root, { recursive: true });
162163
}
163164

165+
// Find a free port starting at the requested one. Mirrors Vite's
166+
// behaviour: shift up by one and try again, up to 10 attempts. We probe
167+
// by binding a throwaway server on the same host the spawned JSS will
168+
// use, so the result reflects the actual interface we'll claim.
169+
async function findFreePort(startPort, host, maxTries = 10) {
170+
for (let p = startPort; p < startPort + maxTries; p++) {
171+
const free = await new Promise((resolve) => {
172+
const srv = createServer();
173+
srv.once('error', () => resolve(false));
174+
srv.once('listening', () => srv.close(() => resolve(true)));
175+
srv.listen(p, host);
176+
});
177+
if (free) return p;
178+
}
179+
return null;
180+
}
181+
182+
const requestedPort = options.port;
183+
const freePort = await findFreePort(requestedPort, options.host);
184+
if (freePort === null) {
185+
console.error(chalk.red(`✗ No free port in range ${requestedPort}-${requestedPort + 9} on ${options.host}.`));
186+
console.error(chalk.dim('Pass --port <number> to pick a different starting port.'));
187+
process.exit(1);
188+
}
189+
if (freePort !== requestedPort) {
190+
console.log(chalk.yellow(`Port ${requestedPort} is in use, using ${freePort} instead.`));
191+
}
192+
options.port = freePort;
193+
164194
// Resolve the JWT signing secret. Priority:
165195
// 1. TOKEN_SECRET env var (operator-controlled)
166196
// 2. Persisted random secret at <root>/.token-secret (generated on

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jspod",
3-
"version": "0.0.15",
3+
"version": "0.0.16",
44
"description": "JavaScript Solid Pod - Just works, batteries included",
55
"type": "module",
66
"main": "index.js",

0 commit comments

Comments
 (0)