laxy-verify 1.3.1 → 1.3.2

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 CHANGED
@@ -161,6 +161,8 @@ Or enable them in `.laxy.yml` and just run:
161
161
  npx laxy-verify .
162
162
  ```
163
163
 
164
+ If the default or configured dev port is already occupied, `laxy-verify` automatically starts its temporary verification server on the next available port. Use `--port` only when you want to attach to an already-running server yourself.
165
+
164
166
  ## Example output
165
167
 
166
168
  ```text
@@ -432,6 +434,11 @@ It is a pre-merge and pre-release verification layer, not your entire quality sy
432
434
 
433
435
  ## Changelog
434
436
 
437
+ ### v1.3.2 - Auto port fallback
438
+
439
+ - If port `3000` or your configured verification port is already in use, `laxy-verify` now starts its temporary dev server on the next available port automatically
440
+ - `--port` keeps its existing meaning: attach to an already-running server instead of starting a temporary one
441
+
435
442
  ### v1.3.1 - Publish fix
436
443
 
437
444
  - Fix npm package contents so `login`, `logout`, and `whoami` ship with the required `dist/init-analysis.js` runtime dependency
package/dist/cli.js CHANGED
@@ -113,12 +113,6 @@ function exitGracefully(code) {
113
113
  }
114
114
  process.exit(code);
115
115
  }
116
- async function ensurePortAvailableForVerification(port) {
117
- const status = await (0, serve_js_1.probeServerStatus)(port);
118
- if (status === null)
119
- return;
120
- throw new Error(`An existing local server is already responding on port ${port} (HTTP ${status}). Stop the running app before using laxy-verify, because the verification build can invalidate an active dev session.`);
121
- }
122
116
  function parseArgs() {
123
117
  const raw = process.argv.slice(2);
124
118
  let projectDir = ".";
@@ -405,7 +399,7 @@ async function run() {
405
399
  --config <path> Path to .laxy.yml
406
400
  --fail-on unverified | bronze | silver | gold
407
401
  --skip-lighthouse Skip Lighthouse but still run build and E2E
408
- --port <port> Use an already-running dev server on this port (skip build & server start)
402
+ --port <port> Use an already-running dev server on this port (skip build & server start)
409
403
  --share Create a public share link for this verification result (Pro)
410
404
  --compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
411
405
  --plan-override free | pro | team (testing metadata only)
@@ -427,9 +421,10 @@ async function run() {
427
421
  2 Configuration error
428
422
 
429
423
  Examples:
430
- npx laxy-verify --init --run # Setup + first verification
431
- npx laxy-verify . # Run in current directory
432
- npx laxy-verify . --ci # CI mode
424
+ npx laxy-verify --init --run # Setup + first verification
425
+ npx laxy-verify . # Run in current directory
426
+ npx laxy-verify . # Auto-falls back if port 3000 is already busy
427
+ npx laxy-verify . --ci # CI mode
433
428
  npx laxy-verify . --fail-on silver # Block Bronze or worse
434
429
  npx laxy-verify . --port 3001 # Use existing dev server on port 3001
435
430
  npx laxy-verify . --share # Save and share a public verification link
@@ -566,28 +561,25 @@ async function run() {
566
561
  }
567
562
  const buildCmd = config.build_command || detected.buildCmd;
568
563
  const devCmd = config.dev_command || detected.devCmd;
569
- const port = args.port ?? config.port;
564
+ const requestedPort = args.port ?? config.port;
570
565
  const useExistingServer = args.port !== undefined;
566
+ let verificationPort = requestedPort;
571
567
  if (!useExistingServer) {
572
- try {
573
- await ensurePortAvailableForVerification(port);
574
- }
575
- catch (err) {
576
- console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
577
- exitGracefully(2);
578
- return;
568
+ verificationPort = await (0, serve_js_1.findAvailablePort)(requestedPort);
569
+ if (verificationPort !== requestedPort && args.format !== "json") {
570
+ console.log(`Port ${requestedPort} is busy. laxy-verify will start its temporary dev server on port ${verificationPort} for this run.`);
579
571
  }
580
572
  }
581
573
  else {
582
574
  // Verify the existing server is actually reachable
583
- const status = await (0, serve_js_1.probeServerStatus)(port);
575
+ const status = await (0, serve_js_1.probeServerStatus)(verificationPort);
584
576
  if (status === null) {
585
- console.error(`Preflight error: No server responding on port ${port}. Make sure the dev server is running before using --port.`);
577
+ console.error(`Preflight error: No server responding on port ${verificationPort}. Make sure the dev server is running before using --port.`);
586
578
  exitGracefully(2);
587
579
  return;
588
580
  }
589
581
  if (args.format !== "json") {
590
- console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
582
+ console.log(`Using existing dev server on port ${verificationPort} (HTTP ${status})`);
591
583
  }
592
584
  }
593
585
  let buildResult;
@@ -677,11 +669,12 @@ async function run() {
677
669
  let servePid;
678
670
  try {
679
671
  if (!useExistingServer) {
680
- const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
672
+ const serve = await (0, serve_js_1.startDevServer)(devCmd, verificationPort, config.dev_timeout, args.projectDir);
681
673
  servePid = serve.pid;
682
674
  activeDevServerPid = serve.pid;
675
+ verificationPort = serve.port;
683
676
  }
684
- const verifyUrl = `http://localhost:${port}/`;
677
+ const verifyUrl = `http://localhost:${verificationPort}/`;
685
678
  const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
686
679
  try {
687
680
  const runtimeRouteDiscovery = await (0, route_discovery_js_1.discoverRuntimeRoutes)(verifyUrl);
@@ -702,13 +695,13 @@ async function run() {
702
695
  : undefined;
703
696
  if (explicitRoutes && explicitRoutes.length > 0) {
704
697
  // Multi-route mode: run on all explicitly configured routes
705
- multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, explicitRoutes);
698
+ multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(verificationPort, config.lighthouse_runs, explicitRoutes);
706
699
  lighthouseErrorCount = multiRouteLhResult.perRoute.reduce((n, r) => n + r.errors.length, 0);
707
700
  scores = multiRouteLhResult.aggregated ?? undefined;
708
701
  }
709
702
  else {
710
703
  // Single-route mode (default): root only
711
- const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
704
+ const lhResult = await (0, lighthouse_js_1.runLighthouse)(verificationPort, config.lighthouse_runs);
712
705
  lighthouseErrorCount = lhResult.errors.length;
713
706
  scores = lhResult.scores ?? undefined;
714
707
  }
@@ -728,7 +721,7 @@ async function run() {
728
721
  }
729
722
  if (!args.skipLighthouse) {
730
723
  try {
731
- multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
724
+ multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(verificationPort);
732
725
  (0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
733
726
  allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
734
727
  if (multiViewportScores.screenshotDiffs) {
@@ -844,7 +837,7 @@ async function run() {
844
837
  ...lastCrawlRoutes,
845
838
  ]).slice(0, config.max_lighthouse_routes);
846
839
  console.log(`\n Running Lighthouse on ${crawlRoutes.length} discovered routes...`);
847
- multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, crawlRoutes);
840
+ multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(verificationPort, config.lighthouse_runs, crawlRoutes);
848
841
  if (multiRouteLhResult.aggregated) {
849
842
  scores = multiRouteLhResult.aggregated;
850
843
  lighthouseResult = {
@@ -895,7 +888,7 @@ async function run() {
895
888
  // Mobile Lighthouse (standalone — not part of multi-viewport)
896
889
  if (!args.skipLighthouse && !args.multiViewport) {
897
890
  try {
898
- mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(port);
891
+ mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(verificationPort);
899
892
  }
900
893
  catch (mlErr) {
901
894
  console.error(`Mobile Lighthouse error: ${mlErr instanceof Error ? mlErr.message : String(mlErr)}`);
@@ -984,7 +977,7 @@ async function run() {
984
977
  // Pro: multi-environment comparison
985
978
  if (effectiveFeatures.compare_env && args.compareUrl) {
986
979
  try {
987
- compareEnvResult = await (0, compare_env_js_1.runEnvComparison)(port, args.compareUrl, 1);
980
+ compareEnvResult = await (0, compare_env_js_1.runEnvComparison)(verificationPort, args.compareUrl, 1);
988
981
  }
989
982
  catch (cmpErr) {
990
983
  console.error(`Env comparison error: ${cmpErr instanceof Error ? cmpErr.message : String(cmpErr)}`);
package/dist/serve.d.ts CHANGED
@@ -9,5 +9,6 @@ export interface ServeResult {
9
9
  port: number;
10
10
  }
11
11
  export declare function probeServerStatus(port: number): Promise<number | null>;
12
+ export declare function findAvailablePort(preferredPort: number, maxAttempts?: number): Promise<number>;
12
13
  export declare function startDevServer(command: string, port: number, timeoutSec: number, cwd?: string): Promise<ServeResult>;
13
14
  export declare function stopDevServer(pid: number): Promise<void>;
package/dist/serve.js CHANGED
@@ -38,11 +38,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.DevServerTimeoutError = exports.PortConflictError = void 0;
40
40
  exports.probeServerStatus = probeServerStatus;
41
+ exports.findAvailablePort = findAvailablePort;
41
42
  exports.startDevServer = startDevServer;
42
43
  exports.stopDevServer = stopDevServer;
43
44
  const node_child_process_1 = require("node:child_process");
44
45
  const fs = __importStar(require("node:fs"));
45
46
  const http = __importStar(require("node:http"));
47
+ const net = __importStar(require("node:net"));
46
48
  const os = __importStar(require("node:os"));
47
49
  const path = __importStar(require("node:path"));
48
50
  const tree_kill_1 = __importDefault(require("tree-kill"));
@@ -158,6 +160,40 @@ function httpGet(url) {
158
160
  function probeServerStatus(port) {
159
161
  return httpGet(`http://localhost:${port}/`);
160
162
  }
163
+ async function canBindPort(port) {
164
+ return new Promise((resolve) => {
165
+ const server = net.createServer();
166
+ server.unref?.();
167
+ server.once("error", () => {
168
+ resolve(false);
169
+ });
170
+ server.listen(port, () => {
171
+ server.close(() => resolve(true));
172
+ });
173
+ });
174
+ }
175
+ async function findAvailablePort(preferredPort, maxAttempts = 20) {
176
+ for (let offset = 0; offset <= maxAttempts; offset++) {
177
+ const candidate = preferredPort + offset;
178
+ if (await canBindPort(candidate)) {
179
+ return candidate;
180
+ }
181
+ }
182
+ return new Promise((resolve, reject) => {
183
+ const server = net.createServer();
184
+ server.unref?.();
185
+ server.once("error", reject);
186
+ server.listen(0, () => {
187
+ const address = server.address();
188
+ if (!address || typeof address === "string") {
189
+ server.close(() => reject(new Error("Failed to resolve an available port.")));
190
+ return;
191
+ }
192
+ const { port } = address;
193
+ server.close(() => resolve(port));
194
+ });
195
+ });
196
+ }
161
197
  async function startDevServer(command, port, timeoutSec, cwd) {
162
198
  return new Promise((resolve, reject) => {
163
199
  console.log(`Starting dev server: ${command}${cwd ? ` (cwd: ${cwd})` : ""}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",