Skip to content

Commit edb69c3

Browse files
adriandlamvercel[bot]Copilot
authored
fix: port detection, postgres nitro apps and nitro app testing (#363)
* add debug logs * add polling for port test * nuxt may be hmring on ignored files/folders * Revert "nuxt may be hmring on ignored files/folders" This reverts commit c57c3f0. * Update packages/utils/src/get-port.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * Update packages/world-local/src/config.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * Update packages/utils/src/get-port.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * revert getPort * test new pid to port cmd * extend sleep time on tests * fix * fix race condition in tests * add logs * add fallback from pid-port * remove pid-port fallback * revert config ts * test: add getPort test * update tests for concurrent calls * temp * trigger rebuild * feat: add windows get port support * fix port sorting * fix parsing logic and filtering * fformat * update ports logi * revert * windows port hack * refactor: optional chaining on windows * test: change ordering of config tests * remove wrong test * simplify windows getPort impl * use execFileSync instead of execFile * use execa * disable test cache * Update packages/utils/src/get-port.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * update * debug * windows cmd * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * lockfile * fix: windows port detection * simplify stuff * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * test: revert gh copilot * fix * increase sleep * revert logs * revert turbo * revert * revert * changeset * Update packages/utils/src/get-port.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * format * init postgres world for express and hono * add posgres world to nitro config * add postgres world start to sveltekit * add postgres world plugin to nitro apps * Update workbench/sveltekit/src/hooks.server.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * Update workbench/vite/vite.config.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * fix nuxt plugin * revert vercel compiled hook on nitro * . * revert * update * CI WILL BE GREEN * remove log in nitro --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]>
1 parent 10ce313 commit edb69c3

File tree

18 files changed

+315
-136
lines changed

18 files changed

+315
-136
lines changed

.changeset/red-ears-smoke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/world-local": patch
3+
"@workflow/utils": patch
4+
---
5+
6+
Fix port detection and base URL resolution for dev servers

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ jobs:
188188
run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }}
189189

190190
- name: Run E2E Tests
191-
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
191+
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
192192
env:
193193
APP_NAME: ${{ matrix.app.name }}
194194
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '5173' || '3000' }}"

packages/utils/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"vitest": "catalog:"
4141
},
4242
"dependencies": {
43-
"ms": "2.1.3",
44-
"pid-port": "2.0.0"
43+
"execa": "9.6.0",
44+
"ms": "2.1.3"
4545
}
4646
}
Lines changed: 141 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,165 @@
11
import http from 'node:http';
2-
import { describe, expect, it } from 'vitest';
2+
import type { AddressInfo } from 'node:net';
3+
import { afterEach, describe, expect, it } from 'vitest';
34
import { getPort } from './get-port';
45

56
describe('getPort', () => {
6-
it('should return undefined or a positive number', async () => {
7+
let servers: http.Server[] = [];
8+
9+
afterEach(() => {
10+
servers.forEach((server) => {
11+
server.close();
12+
});
13+
servers = [];
14+
});
15+
16+
it('should return undefined when no ports are in use', async () => {
717
const port = await getPort();
8-
expect(port === undefined || typeof port === 'number').toBe(true);
9-
if (port !== undefined) {
10-
expect(port).toBeGreaterThan(0);
11-
}
18+
19+
expect(port).toBeUndefined();
20+
});
21+
22+
it('should handle servers listening on specific ports', async () => {
23+
const server = http.createServer();
24+
servers.push(server);
25+
26+
// Listen on a specific port instead of 0
27+
const specificPort = 3000;
28+
server.listen(specificPort);
29+
30+
const port = await getPort();
31+
32+
expect(port).toEqual(specificPort);
1233
});
1334

14-
it('should return a port number when a server is listening', async () => {
35+
it('should return the port number that the server is listening', async () => {
1536
const server = http.createServer();
37+
servers.push(server);
1638

1739
server.listen(0);
1840

19-
try {
20-
const port = await getPort();
21-
const address = server.address();
22-
23-
// Port detection may not work immediately in all environments (CI, Docker, etc.)
24-
// so we just verify the function returns a valid result
25-
if (port !== undefined) {
26-
expect(typeof port).toBe('number');
27-
expect(port).toBeGreaterThan(0);
28-
29-
// If we have the address, optionally verify it matches
30-
if (address && typeof address === 'object') {
31-
// In most cases it should match, but not required for test to pass
32-
expect([port, undefined]).toContain(port);
33-
}
34-
}
35-
} finally {
36-
await new Promise<void>((resolve, reject) => {
37-
server.close((err) => (err ? reject(err) : resolve()));
38-
});
39-
}
41+
const port = await getPort();
42+
const addr = server.address() as AddressInfo;
43+
44+
expect(typeof port).toBe('number');
45+
expect(port).toEqual(addr.port);
4046
});
4147

42-
it('should return the smallest port when multiple servers are listening', async () => {
48+
it('should return the first port of the server', async () => {
4349
const server1 = http.createServer();
4450
const server2 = http.createServer();
51+
servers.push(server1);
52+
servers.push(server2);
4553

4654
server1.listen(0);
4755
server2.listen(0);
4856

57+
const port = await getPort();
58+
const addr1 = server1.address() as AddressInfo;
59+
60+
expect(port).toEqual(addr1.port);
61+
});
62+
63+
it('should return consistent results when called multiple times', async () => {
64+
const server = http.createServer();
65+
servers.push(server);
66+
server.listen(0);
67+
68+
const port1 = await getPort();
69+
const port2 = await getPort();
70+
const port3 = await getPort();
71+
72+
expect(port1).toEqual(port2);
73+
expect(port2).toEqual(port3);
74+
});
75+
76+
it('should handle IPv6 addresses', async () => {
77+
const server = http.createServer();
78+
servers.push(server);
79+
4980
try {
81+
server.listen(0, '::1'); // IPv6 localhost
5082
const port = await getPort();
51-
const addr1 = server1.address();
52-
const addr2 = server2.address();
53-
54-
// Port detection may not work in all environments
55-
if (
56-
port !== undefined &&
57-
addr1 &&
58-
typeof addr1 === 'object' &&
59-
addr2 &&
60-
typeof addr2 === 'object'
61-
) {
62-
// Should return the smallest port
63-
expect(port).toBeLessThanOrEqual(Math.max(addr1.port, addr2.port));
64-
expect(port).toBeGreaterThan(0);
65-
} else {
66-
// If port detection doesn't work in this environment, just pass
67-
expect(port === undefined || typeof port === 'number').toBe(true);
68-
}
69-
} finally {
70-
await Promise.all([
71-
new Promise<void>((resolve, reject) => {
72-
server1.close((err) => (err ? reject(err) : resolve()));
73-
}),
74-
new Promise<void>((resolve, reject) => {
75-
server2.close((err) => (err ? reject(err) : resolve()));
76-
}),
77-
]);
83+
const addr = server.address() as AddressInfo;
84+
85+
expect(port).toEqual(addr.port);
86+
} catch {
87+
// Skip test if IPv6 is not available
88+
console.log('IPv6 not available, skipping test');
7889
}
7990
});
91+
92+
it('should handle multiple calls in sequence', async () => {
93+
const server = http.createServer();
94+
servers.push(server);
95+
96+
server.listen(0);
97+
98+
const port1 = await getPort();
99+
const port2 = await getPort();
100+
const addr = server.address() as AddressInfo;
101+
102+
// Should return the same port each time
103+
expect(port1).toEqual(addr.port);
104+
expect(port2).toEqual(addr.port);
105+
});
106+
107+
it('should handle closed servers', async () => {
108+
const server = http.createServer();
109+
110+
server.listen(0);
111+
const addr = server.address() as AddressInfo;
112+
const serverPort = addr.port;
113+
114+
// Close the server before calling getPort
115+
server.close();
116+
117+
const port = await getPort();
118+
119+
// Port should not be the closed server's port
120+
expect(port).not.toEqual(serverPort);
121+
});
122+
123+
it('should handle server restart on same port', async () => {
124+
const server1 = http.createServer();
125+
servers.push(server1);
126+
server1.listen(3000);
127+
128+
const port1 = await getPort();
129+
expect(port1).toEqual(3000);
130+
131+
server1.close();
132+
servers = servers.filter((s) => s !== server1);
133+
134+
// Small delay to ensure port is released
135+
await new Promise((resolve) => setTimeout(resolve, 100));
136+
137+
const server2 = http.createServer();
138+
servers.push(server2);
139+
server2.listen(3000);
140+
141+
const port2 = await getPort();
142+
expect(port2).toEqual(3000);
143+
});
144+
145+
it('should handle concurrent getPort calls', async () => {
146+
// Workflow makes lots of concurrent getPort calls
147+
const server = http.createServer();
148+
servers.push(server);
149+
server.listen(0);
150+
151+
const addr = server.address() as AddressInfo;
152+
153+
// Call getPort concurrently 10 times
154+
const results = await Promise.all(
155+
Array(10)
156+
.fill(0)
157+
.map(() => getPort())
158+
);
159+
160+
// All should return the same port without errors
161+
results.forEach((port) => {
162+
expect(port).toEqual(addr.port);
163+
});
164+
});
80165
});

packages/utils/src/get-port.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,70 @@
1-
import { pidToPorts } from 'pid-port';
1+
import { execa } from 'execa';
22

33
/**
44
* Gets the port number that the process is listening on.
55
* @returns The port number that the process is listening on, or undefined if the process is not listening on any port.
6-
* NOTE: Can't move this to @workflow/utils because it's being imported into @workflow/errors for RetryableError (inside workflow runtime)
76
*/
87
export async function getPort(): Promise<number | undefined> {
8+
const { pid, platform } = process;
9+
10+
let port: number | undefined;
11+
912
try {
10-
const pid = process.pid;
11-
const ports = await pidToPorts(pid);
12-
if (!ports || ports.size === 0) {
13-
return undefined;
14-
}
13+
switch (platform) {
14+
case 'linux':
15+
case 'darwin': {
16+
const lsofResult = await execa('lsof', [
17+
'-a',
18+
'-i',
19+
'-P',
20+
'-n',
21+
'-p',
22+
pid.toString(),
23+
]);
24+
const awkResult = await execa(
25+
'awk',
26+
['/LISTEN/ {split($9,a,":"); print a[length(a)]; exit}'],
27+
{
28+
input: lsofResult.stdout,
29+
}
30+
);
31+
port = parseInt(awkResult.stdout.trim(), 10);
32+
break;
33+
}
34+
35+
case 'win32': {
36+
// Use cmd to run the piped command
37+
const result = await execa('cmd', [
38+
'/c',
39+
`netstat -ano | findstr ${pid} | findstr LISTENING`,
40+
]);
1541

16-
const smallest = Math.min(...ports);
17-
return smallest;
18-
} catch {
19-
// If port detection fails (e.g., `ss` command not available in production),
20-
// return undefined and fall back to default port
42+
const stdout = result.stdout.trim();
43+
44+
if (stdout) {
45+
const lines = stdout.split('\n');
46+
for (const line of lines) {
47+
// Extract port from the local address column
48+
// Matches both IPv4 (e.g., "127.0.0.1:3000") and IPv6 bracket notation (e.g., "[::1]:3000")
49+
const match = line
50+
.trim()
51+
.match(/^\s*TCP\s+(?:\[[\da-f:]+\]|[\d.]+):(\d+)\s+/i);
52+
if (match) {
53+
port = parseInt(match[1], 10);
54+
break;
55+
}
56+
}
57+
}
58+
break;
59+
}
60+
}
61+
} catch (error) {
62+
// In dev, it's helpful to know why detection failed
63+
if (process.env.NODE_ENV === 'development') {
64+
console.debug('[getPort] Detection failed:', error);
65+
}
2166
return undefined;
2267
}
68+
69+
return Number.isNaN(port) ? undefined : port;
2370
}

0 commit comments

Comments
 (0)