Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
51b7160
add debug logs
adriandlam Nov 18, 2025
90cfebb
add polling for port test
adriandlam Nov 19, 2025
04fda69
nuxt may be hmring on ignored files/folders
adriandlam Nov 19, 2025
101fd54
Revert "nuxt may be hmring on ignored files/folders"
adriandlam Nov 19, 2025
16d54f6
Update packages/utils/src/get-port.ts
adriandlam Nov 19, 2025
1e64d5d
Update packages/world-local/src/config.ts
adriandlam Nov 20, 2025
22d1e7c
Update packages/utils/src/get-port.ts
adriandlam Nov 20, 2025
0820c23
revert getPort
adriandlam Nov 20, 2025
c72742b
test new pid to port cmd
adriandlam Nov 20, 2025
a9228b4
extend sleep time on tests
adriandlam Nov 20, 2025
0a89c37
fix
adriandlam Nov 20, 2025
90d3b21
fix race condition in tests
adriandlam Nov 20, 2025
28afae3
add logs
adriandlam Nov 20, 2025
440a702
add fallback from pid-port
adriandlam Nov 20, 2025
cde4a51
remove pid-port fallback
adriandlam Nov 20, 2025
3aa7f80
revert config ts
adriandlam Nov 20, 2025
a53a84b
test: add getPort test
adriandlam Nov 20, 2025
90f3c3d
update tests for concurrent calls
adriandlam Nov 20, 2025
6c86870
temp
adriandlam Nov 20, 2025
fbb557a
trigger rebuild
adriandlam Nov 20, 2025
ec106fb
feat: add windows get port support
adriandlam Nov 20, 2025
27f8e5e
fix port sorting
adriandlam Nov 20, 2025
d18a210
fix parsing logic and filtering
adriandlam Nov 20, 2025
da85ce0
fformat
adriandlam Nov 20, 2025
320d63e
update ports logi
adriandlam Nov 20, 2025
1d6920f
revert
adriandlam Nov 20, 2025
2c8e3b7
windows port hack
adriandlam Nov 20, 2025
950720a
refactor: optional chaining on windows
adriandlam Nov 20, 2025
80369c6
test: change ordering of config tests
adriandlam Nov 21, 2025
9477405
remove wrong test
adriandlam Nov 21, 2025
db44b66
simplify windows getPort impl
adriandlam Nov 21, 2025
58435f2
use execFileSync instead of execFile
adriandlam Nov 21, 2025
9105681
use execa
adriandlam Nov 21, 2025
3f8aa90
disable test cache
adriandlam Nov 21, 2025
4e2f574
Update packages/utils/src/get-port.ts
adriandlam Nov 21, 2025
5955328
update
adriandlam Nov 21, 2025
e8cd7d1
debug
adriandlam Nov 21, 2025
e4d067f
windows cmd
adriandlam Nov 21, 2025
78506cb
Apply suggestion from @Copilot
adriandlam Nov 21, 2025
eb35c3b
Apply suggestion from @Copilot
adriandlam Nov 21, 2025
0a2376f
lockfile
adriandlam Nov 21, 2025
0412a8c
fix: windows port detection
adriandlam Nov 22, 2025
ec2162f
simplify stuff
adriandlam Nov 22, 2025
37c4481
Apply suggestion from @Copilot
adriandlam Nov 22, 2025
6a43f2c
test: revert gh copilot
adriandlam Nov 22, 2025
32850cd
fix
adriandlam Nov 22, 2025
1fc7f73
increase sleep
adriandlam Nov 22, 2025
a492a2e
revert logs
adriandlam Nov 24, 2025
32f74ab
revert turbo
adriandlam Nov 24, 2025
19098ba
revert
adriandlam Nov 24, 2025
7c24436
revert
adriandlam Nov 24, 2025
a5d25c9
changeset
adriandlam Nov 24, 2025
5bdc023
Update packages/utils/src/get-port.ts
adriandlam Nov 24, 2025
41c5ee2
format
adriandlam Nov 24, 2025
56e4c9c
init postgres world for express and hono
adriandlam Nov 24, 2025
0afd8a4
add posgres world to nitro config
adriandlam Nov 24, 2025
c5f6434
add postgres world start to sveltekit
adriandlam Nov 24, 2025
ebff0a3
add postgres world plugin to nitro apps
adriandlam Nov 24, 2025
25e714a
Update workbench/sveltekit/src/hooks.server.ts
adriandlam Nov 24, 2025
3f4a23c
Update workbench/vite/vite.config.ts
adriandlam Nov 24, 2025
d9e60ef
fix nuxt plugin
adriandlam Nov 24, 2025
19ca4e2
revert vercel compiled hook on nitro
adriandlam Nov 24, 2025
f43e1c0
.
adriandlam Nov 24, 2025
d618959
revert
adriandlam Nov 24, 2025
f467071
update
adriandlam Nov 24, 2025
cbd4f9c
CI WILL BE GREEN
adriandlam Nov 24, 2025
ae1b490
remove log in nitro
adriandlam Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/red-ears-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/world-local": patch
"@workflow/utils": patch
---

Fix port detection and base URL resolution for dev servers
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ jobs:
run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }}

- name: Run E2E Tests
run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/dev.test.ts && pnpm run test:e2e
run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/dev.test.ts && sleep 10 && pnpm run test:e2e
env:
APP_NAME: ${{ matrix.app.name }}
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '5173' || '3000' }}"
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"vitest": "catalog:"
},
"dependencies": {
"ms": "2.1.3",
"pid-port": "2.0.0"
"execa": "9.6.0",
"ms": "2.1.3"
}
}
197 changes: 141 additions & 56 deletions packages/utils/src/get-port.test.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,165 @@
import http from 'node:http';
import { describe, expect, it } from 'vitest';
import type { AddressInfo } from 'node:net';
import { afterEach, describe, expect, it } from 'vitest';
import { getPort } from './get-port';

describe('getPort', () => {
it('should return undefined or a positive number', async () => {
let servers: http.Server[] = [];

afterEach(() => {
servers.forEach((server) => {
server.close();
});
servers = [];
});

it('should return undefined when no ports are in use', async () => {
const port = await getPort();
expect(port === undefined || typeof port === 'number').toBe(true);
if (port !== undefined) {
expect(port).toBeGreaterThan(0);
}

expect(port).toBeUndefined();
});

it('should handle servers listening on specific ports', async () => {
const server = http.createServer();
servers.push(server);

// Listen on a specific port instead of 0
const specificPort = 3000;
server.listen(specificPort);

const port = await getPort();

expect(port).toEqual(specificPort);
});

it('should return a port number when a server is listening', async () => {
it('should return the port number that the server is listening', async () => {
const server = http.createServer();
servers.push(server);

server.listen(0);

try {
const port = await getPort();
const address = server.address();

// Port detection may not work immediately in all environments (CI, Docker, etc.)
// so we just verify the function returns a valid result
if (port !== undefined) {
expect(typeof port).toBe('number');
expect(port).toBeGreaterThan(0);

// If we have the address, optionally verify it matches
if (address && typeof address === 'object') {
// In most cases it should match, but not required for test to pass
expect([port, undefined]).toContain(port);
}
}
} finally {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
}
const port = await getPort();
const addr = server.address() as AddressInfo;

expect(typeof port).toBe('number');
expect(port).toEqual(addr.port);
});

it('should return the smallest port when multiple servers are listening', async () => {
it('should return the first port of the server', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assumes getPort() will return server1's port, but the implementation returns the first listening port found by lsof/netstat, which may not correspond to server1 if connections are listed in a different order.

View Details
📝 Patch Details
diff --git a/packages/utils/src/get-port.test.ts b/packages/utils/src/get-port.test.ts
index 0dbad85..3b21784 100644
--- a/packages/utils/src/get-port.test.ts
+++ b/packages/utils/src/get-port.test.ts
@@ -45,7 +45,7 @@ describe('getPort', () => {
     expect(port).toEqual(addr.port);
   });
 
-  it('should return the first port of the server', async () => {
+  it('should return the smallest port when multiple servers are listening', async () => {
     const server1 = http.createServer();
     const server2 = http.createServer();
     servers.push(server1);
@@ -56,8 +56,10 @@ describe('getPort', () => {
 
     const port = await getPort();
     const addr1 = server1.address() as AddressInfo;
+    const addr2 = server2.address() as AddressInfo;
 
-    expect(port).toEqual(addr1.port);
+    const smallestPort = Math.min(addr1.port, addr2.port);
+    expect(port).toEqual(smallestPort);
   });
 
   it('should return consistent results when called multiple times', async () => {
diff --git a/packages/utils/src/get-port.ts b/packages/utils/src/get-port.ts
index 96eb402..d1eb9a3 100644
--- a/packages/utils/src/get-port.ts
+++ b/packages/utils/src/get-port.ts
@@ -7,7 +7,7 @@ import { execa } from 'execa';
 export async function getPort(): Promise<number | undefined> {
   const { pid, platform } = process;
 
-  let port: number | undefined;
+  let ports: number[] = [];
 
   try {
     switch (platform) {
@@ -21,14 +21,22 @@ export async function getPort(): Promise<number | undefined> {
           '-p',
           pid.toString(),
         ]);
-        const awkResult = await execa(
-          'awk',
-          ['/LISTEN/ {split($9,a,":"); print a[length(a)]; exit}'],
-          {
-            input: lsofResult.stdout,
+        const lines = lsofResult.stdout.split('\n');
+        for (const line of lines) {
+          if (line.includes('LISTEN')) {
+            const parts = line.split(/\s+/);
+            if (parts.length > 8) {
+              const addr = parts[8];
+              const portMatch = addr.match(/:(\d+)$/);
+              if (portMatch) {
+                const p = parseInt(portMatch[1], 10);
+                if (!Number.isNaN(p) && !ports.includes(p)) {
+                  ports.push(p);
+                }
+              }
+            }
           }
-        );
-        port = parseInt(awkResult.stdout.trim(), 10);
+        }
         break;
       }
 
@@ -47,8 +55,10 @@ export async function getPort(): Promise<number | undefined> {
             // Extract port from the local address column
             const match = line.trim().match(/^\s*TCP\s+[\d.:]+:(\d+)\s+/);
             if (match) {
-              port = parseInt(match[1], 10);
-              break;
+              const p = parseInt(match[1], 10);
+              if (!Number.isNaN(p) && !ports.includes(p)) {
+                ports.push(p);
+              }
             }
           }
         }
@@ -63,5 +73,10 @@ export async function getPort(): Promise<number | undefined> {
     return undefined;
   }
 
-  return Number.isNaN(port) ? undefined : port;
+  if (ports.length === 0) {
+    return undefined;
+  }
+
+  // Return the smallest port for consistent behavior
+  return Math.min(...ports);
 }

Analysis

Test assumes getPort() returns server1's port but implementation returns first port from lsof/netstat

What fails: The test "should return the first port of the server" in packages/utils/src/get-port.test.ts (lines 48-61) makes an invalid assumption about which server's port getPort() will return when multiple servers listen on random ports.

How to reproduce: Create two HTTP servers with random ports and call getPort():

server1.listen(0);
server2.listen(0);
const port = await getPort();

Since OS assigns random ports, server1 doesn't always have a lower port than server2. The original implementation used lsof/netstat which returns the "first" listening socket, but lsof output order is not guaranteed to match server creation order. The test incorrectly assumed getPort() would return server1's port specifically.

Result: The test assertion expect(port).toEqual(addr1.port) fails intermittently on systems where server2's port appears first in lsof output.

Expected: Per the original implementation (commit adf0cfe), getPort() should return the smallest port for predictable behavior. Updated implementation to collect all listening ports and return Math.min(...ports). Test updated to verify the smallest port is returned, not server1's specific port.

Fix implemented:

  1. Modified packages/utils/src/get-port.ts to extract all listening ports instead of just the first one, then return the smallest port via Math.min(...ports)
  2. Updated test name and assertion in packages/utils/src/get-port.test.ts from "should return the first port of the server" expecting addr1.port to "should return the smallest port when multiple servers are listening" expecting Math.min(addr1.port, addr2.port)

const server1 = http.createServer();
const server2 = http.createServer();
servers.push(server1);
servers.push(server2);

server1.listen(0);
server2.listen(0);

const port = await getPort();
const addr1 = server1.address() as AddressInfo;

expect(port).toEqual(addr1.port);
});

it('should return consistent results when called multiple times', async () => {
const server = http.createServer();
servers.push(server);
server.listen(0);

const port1 = await getPort();
const port2 = await getPort();
const port3 = await getPort();

expect(port1).toEqual(port2);
expect(port2).toEqual(port3);
});

it('should handle IPv6 addresses', async () => {
const server = http.createServer();
servers.push(server);

try {
server.listen(0, '::1'); // IPv6 localhost
const port = await getPort();
const addr1 = server1.address();
const addr2 = server2.address();

// Port detection may not work in all environments
if (
port !== undefined &&
addr1 &&
typeof addr1 === 'object' &&
addr2 &&
typeof addr2 === 'object'
) {
// Should return the smallest port
expect(port).toBeLessThanOrEqual(Math.max(addr1.port, addr2.port));
expect(port).toBeGreaterThan(0);
} else {
// If port detection doesn't work in this environment, just pass
expect(port === undefined || typeof port === 'number').toBe(true);
}
} finally {
await Promise.all([
new Promise<void>((resolve, reject) => {
server1.close((err) => (err ? reject(err) : resolve()));
}),
new Promise<void>((resolve, reject) => {
server2.close((err) => (err ? reject(err) : resolve()));
}),
]);
const addr = server.address() as AddressInfo;

expect(port).toEqual(addr.port);
} catch {
// Skip test if IPv6 is not available
console.log('IPv6 not available, skipping test');
}
});

it('should handle multiple calls in sequence', async () => {
const server = http.createServer();
servers.push(server);

server.listen(0);

const port1 = await getPort();
const port2 = await getPort();
const addr = server.address() as AddressInfo;

// Should return the same port each time
expect(port1).toEqual(addr.port);
expect(port2).toEqual(addr.port);
});

it('should handle closed servers', async () => {
const server = http.createServer();

server.listen(0);
const addr = server.address() as AddressInfo;
const serverPort = addr.port;

// Close the server before calling getPort
server.close();

const port = await getPort();

// Port should not be the closed server's port
expect(port).not.toEqual(serverPort);
});

it('should handle server restart on same port', async () => {
const server1 = http.createServer();
servers.push(server1);
server1.listen(3000);

const port1 = await getPort();
expect(port1).toEqual(3000);

server1.close();
servers = servers.filter((s) => s !== server1);

// Small delay to ensure port is released
await new Promise((resolve) => setTimeout(resolve, 100));

const server2 = http.createServer();
servers.push(server2);
server2.listen(3000);

const port2 = await getPort();
expect(port2).toEqual(3000);
});

it('should handle concurrent getPort calls', async () => {
// Workflow makes lots of concurrent getPort calls
const server = http.createServer();
servers.push(server);
server.listen(0);

const addr = server.address() as AddressInfo;

// Call getPort concurrently 10 times
const results = await Promise.all(
Array(10)
.fill(0)
.map(() => getPort())
);

// All should return the same port without errors
results.forEach((port) => {
expect(port).toEqual(addr.port);
});
});
});
71 changes: 59 additions & 12 deletions packages/utils/src/get-port.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,70 @@
import { pidToPorts } from 'pid-port';
import { execa } from 'execa';

/**
* Gets the port number that the process is listening on.
* @returns The port number that the process is listening on, or undefined if the process is not listening on any port.
* NOTE: Can't move this to @workflow/utils because it's being imported into @workflow/errors for RetryableError (inside workflow runtime)
*/
export async function getPort(): Promise<number | undefined> {
const { pid, platform } = process;

let port: number | undefined;

try {
const pid = process.pid;
const ports = await pidToPorts(pid);
if (!ports || ports.size === 0) {
return undefined;
}
switch (platform) {
case 'linux':
case 'darwin': {
const lsofResult = await execa('lsof', [
'-a',
'-i',
'-P',
'-n',
'-p',
pid.toString(),
]);
const awkResult = await execa(
'awk',
['/LISTEN/ {split($9,a,":"); print a[length(a)]; exit}'],
{
input: lsofResult.stdout,
}
);
port = parseInt(awkResult.stdout.trim(), 10);
break;
}

case 'win32': {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to spin up a windows vm for this :sadge:

// Use cmd to run the piped command
const result = await execa('cmd', [
'/c',
`netstat -ano | findstr ${pid} | findstr LISTENING`,
]);

const smallest = Math.min(...ports);
return smallest;
} catch {
// If port detection fails (e.g., `ss` command not available in production),
// return undefined and fall back to default port
const stdout = result.stdout.trim();

if (stdout) {
const lines = stdout.split('\n');
for (const line of lines) {
// Extract port from the local address column
// Matches both IPv4 (e.g., "127.0.0.1:3000") and IPv6 bracket notation (e.g., "[::1]:3000")
const match = line
.trim()
.match(/^\s*TCP\s+(?:\[[\da-f:]+\]|[\d.]+):(\d+)\s+/i);
if (match) {
port = parseInt(match[1], 10);
break;
}
}
}
break;
}
}
} catch (error) {
// In dev, it's helpful to know why detection failed
if (process.env.NODE_ENV === 'development') {
console.debug('[getPort] Detection failed:', error);
}
return undefined;
}

return Number.isNaN(port) ? undefined : port;
}
Loading