laxy-verify 1.3.1 → 1.3.3

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,16 @@ 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.3 - Cleaner output and route validation
438
+
439
+ - Removed `Pro` and `Pro+` labels from verification logs so `npx laxy-verify .` prints plan-neutral output
440
+ - Runtime-discovered routes are now validated before they are used for Lighthouse and broken-link checks, so bogus `404` bundle snippets do not poison the verification result
441
+
442
+ ### v1.3.2 - Auto port fallback
443
+
444
+ - 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
445
+ - `--port` keeps its existing meaning: attach to an already-running server instead of starting a temporary one
446
+
435
447
  ### v1.3.1 - Publish fix
436
448
 
437
449
  - 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)}`);
@@ -38,7 +38,7 @@ exports.printMultiViewportResults = printMultiViewportResults;
38
38
  exports.allViewportsPass = allViewportsPass;
39
39
  exports.runMobileLighthouse = runMobileLighthouse;
40
40
  /**
41
- * Pro+ multi-viewport Lighthouse checks.
41
+ * Multi-viewport Lighthouse checks.
42
42
  *
43
43
  * Each viewport runs through the same direct Lighthouse execution path used by
44
44
  * the main verify flow so Windows cleanup behavior is consistent.
@@ -257,7 +257,7 @@ function compareWithBaseline(viewport, screenshot) {
257
257
  return { viewport: viewport.name, diffPercent: Math.round(diff * 100) / 100, baselineCreated: false };
258
258
  }
259
259
  async function runMultiViewportLighthouse(port) {
260
- console.log("\n [Pro+] Running multi-viewport Lighthouse checks (desktop, tablet, mobile)...");
260
+ console.log("\n Running multi-viewport Lighthouse checks (desktop, tablet, mobile)...");
261
261
  const tempRoot = path.join(process.cwd(), ".laxy-tmp", `multi-viewport-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
262
262
  fs.mkdirSync(tempRoot, { recursive: true });
263
263
  const results = { desktop: null, tablet: null, mobile: null };
@@ -299,7 +299,7 @@ function printMultiViewportResults(scores, thresholds) {
299
299
  tablet: "Tablet",
300
300
  mobile: "Mobile",
301
301
  };
302
- console.log("\n [Pro+] Multi-viewport results:");
302
+ console.log("\n Multi-viewport results:");
303
303
  for (const viewport of labels) {
304
304
  const score = scores[viewport];
305
305
  if (!score) {
@@ -329,17 +329,17 @@ function allViewportsPass(scores, thresholds) {
329
329
  * full multi-viewport overhead. Lets Pro users catch mobile regressions.
330
330
  */
331
331
  async function runMobileLighthouse(port) {
332
- console.log("\n [Pro] Running mobile Lighthouse check...");
332
+ console.log("\n Running mobile Lighthouse check...");
333
333
  const mobileViewport = VIEWPORTS.find((v) => v.name === "mobile");
334
334
  const tempRoot = path.join(process.cwd(), ".laxy-tmp", `mobile-lh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
335
335
  fs.mkdirSync(tempRoot, { recursive: true });
336
336
  try {
337
337
  const scores = await runLighthouseForViewport(port, mobileViewport, tempRoot);
338
338
  if (scores) {
339
- console.log(` [Pro] Mobile: P=${scores.performance} A=${scores.accessibility} SEO=${scores.seo} BP=${scores.bestPractices}`);
339
+ console.log(` Mobile: P=${scores.performance} A=${scores.accessibility} SEO=${scores.seo} BP=${scores.bestPractices}`);
340
340
  }
341
341
  else {
342
- console.log(" [Pro] Mobile Lighthouse: failed to collect");
342
+ console.log(" Mobile Lighthouse: failed to collect");
343
343
  }
344
344
  return scores;
345
345
  }
@@ -4,4 +4,5 @@ export interface RuntimeRouteDiscoveryResult {
4
4
  }
5
5
  export declare function extractScriptUrlsFromHtml(html: string, baseUrl: string): string[];
6
6
  export declare function extractRoutesFromText(content: string): string[];
7
+ export declare function filterReachableRoutes(baseUrl: string, routes: string[]): Promise<string[]>;
7
8
  export declare function discoverRuntimeRoutes(baseUrl: string): Promise<RuntimeRouteDiscoveryResult>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractScriptUrlsFromHtml = extractScriptUrlsFromHtml;
4
4
  exports.extractRoutesFromText = extractRoutesFromText;
5
+ exports.filterReachableRoutes = filterReachableRoutes;
5
6
  exports.discoverRuntimeRoutes = discoverRuntimeRoutes;
6
7
  const SCRIPT_SRC_REGEX = /<script[^>]+src=["']([^"'#?]+(?:\?[^"'#]*)?)["']/gi;
7
8
  const HTML_ROUTE_REGEX = /(?:href|data-href)=["'](\/[^"'#? ]*)/gi;
@@ -72,6 +73,34 @@ function extractRoutesFromText(content) {
72
73
  }
73
74
  return routes;
74
75
  }
76
+ async function probeRuntimeRoute(baseUrl, route) {
77
+ try {
78
+ const res = await fetch(new URL(route, baseUrl), {
79
+ signal: AbortSignal.timeout(5000),
80
+ redirect: "follow",
81
+ headers: { accept: "text/html,application/xhtml+xml" },
82
+ });
83
+ if (res.status === 404 || res.status === 410) {
84
+ return false;
85
+ }
86
+ const contentType = res.headers.get("content-type")?.toLowerCase() ?? "";
87
+ if (res.status >= 200 && res.status < 300 && contentType && !contentType.includes("text/html")) {
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ async function filterReachableRoutes(baseUrl, routes) {
97
+ const candidates = Array.from(new Set(routes));
98
+ const checks = await Promise.all(candidates.map(async (route) => ({
99
+ route,
100
+ reachable: await probeRuntimeRoute(baseUrl, route),
101
+ })));
102
+ return checks.filter((item) => item.reachable).map((item) => item.route);
103
+ }
75
104
  async function discoverRuntimeRoutes(baseUrl) {
76
105
  const htmlRes = await fetch(baseUrl, {
77
106
  signal: AbortSignal.timeout(8000),
@@ -101,8 +130,9 @@ async function discoverRuntimeRoutes(baseUrl) {
101
130
  // Skip chunk fetch failures. This is best-effort coverage expansion.
102
131
  }
103
132
  }));
133
+ const validatedRoutes = await filterReachableRoutes(baseUrl, Array.from(routes));
104
134
  return {
105
- routes: Array.from(routes).sort((a, b) => a.localeCompare(b)),
135
+ routes: validatedRoutes.sort((a, b) => a.localeCompare(b)),
106
136
  scriptUrls,
107
137
  };
108
138
  }
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.3",
4
4
  "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",