diff --git a/.changeset/red-ears-smoke.md b/.changeset/red-ears-smoke.md new file mode 100644 index 000000000..0539de7aa --- /dev/null +++ b/.changeset/red-ears-smoke.md @@ -0,0 +1,6 @@ +--- +"@workflow/world-local": patch +"@workflow/utils": patch +--- + +Fix port detection and base URL resolution for dev servers diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8aeefb8af..7bd31d34f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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' }}" diff --git a/packages/utils/package.json b/packages/utils/package.json index c040cc952..fdb23ab77 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -40,7 +40,7 @@ "vitest": "catalog:" }, "dependencies": { - "ms": "2.1.3", - "pid-port": "2.0.0" + "execa": "9.6.0", + "ms": "2.1.3" } } diff --git a/packages/utils/src/get-port.test.ts b/packages/utils/src/get-port.test.ts index fe1d94dfe..0dbad8531 100644 --- a/packages/utils/src/get-port.test.ts +++ b/packages/utils/src/get-port.test.ts @@ -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((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 () => { 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((resolve, reject) => { - server1.close((err) => (err ? reject(err) : resolve())); - }), - new Promise((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); + }); + }); }); diff --git a/packages/utils/src/get-port.ts b/packages/utils/src/get-port.ts index ac225d8e4..21d5a98be 100644 --- a/packages/utils/src/get-port.ts +++ b/packages/utils/src/get-port.ts @@ -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 { + 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': { + // 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; } diff --git a/packages/world-local/src/config.test.ts b/packages/world-local/src/config.test.ts index 6ea983dd7..c3761ebe6 100644 --- a/packages/world-local/src/config.test.ts +++ b/packages/world-local/src/config.test.ts @@ -46,36 +46,36 @@ describe('resolveBaseUrl', () => { expect(getPort).not.toHaveBeenCalled(); }); - it('should auto-detect port when neither baseUrl nor port is provided', async () => { + it('should use PORT env var when neither baseUrl nor port is provided', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(5173); process.env.PORT = '8080'; const result = await resolveBaseUrl({}); - expect(result).toBe('http://localhost:5173'); - expect(getPort).toHaveBeenCalled(); + expect(result).toBe('http://localhost:8080'); + expect(getPort).not.toHaveBeenCalled(); }); - it('should use PORT env var when auto-detection returns undefined', async () => { + it('should use auto-detected port when PORT env var is not set', async () => { const { getPort } = await import('@workflow/utils/get-port'); - vi.mocked(getPort).mockResolvedValue(undefined); - process.env.PORT = '8080'; + vi.mocked(getPort).mockResolvedValue(5173); + delete process.env.PORT; const result = await resolveBaseUrl({}); - expect(result).toBe('http://localhost:8080'); + expect(result).toBe('http://localhost:5173'); expect(getPort).toHaveBeenCalled(); }); - it('should fallback to 3000 when all detection methods fail', async () => { + it('should throw error when all detection methods fail', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(undefined); delete process.env.PORT; - const result = await resolveBaseUrl({}); - - expect(result).toBe('http://localhost:3000'); + await expect(resolveBaseUrl({})).rejects.toThrow( + 'Unable to resolve base URL for workflow queue.' + ); expect(getPort).toHaveBeenCalled(); }); }); @@ -155,6 +155,7 @@ describe('resolveBaseUrl', () => { it('should use auto-detected port for SvelteKit default (5173)', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(5173); + delete process.env.PORT; const result = await resolveBaseUrl({}); @@ -164,6 +165,7 @@ describe('resolveBaseUrl', () => { it('should use auto-detected port for Vite default (5173)', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(5173); + delete process.env.PORT; const result = await resolveBaseUrl({}); @@ -173,20 +175,21 @@ describe('resolveBaseUrl', () => { it('should use auto-detected port for Next.js default (3000)', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(3000); + delete process.env.PORT; const result = await resolveBaseUrl({}); expect(result).toBe('http://localhost:3000'); }); - it('should handle auto-detection failure gracefully', async () => { + it('should throw error when auto-detection fails', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(undefined); delete process.env.PORT; - const result = await resolveBaseUrl({}); - - expect(result).toBe('http://localhost:3000'); + await expect(resolveBaseUrl({})).rejects.toThrow( + 'Unable to resolve base URL for workflow queue.' + ); }); }); @@ -227,40 +230,43 @@ describe('resolveBaseUrl', () => { }); describe('edge cases', () => { - it('should handle empty config object', async () => { + it('should throw error with empty config object when no port is detected', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(undefined); delete process.env.PORT; - const result = await resolveBaseUrl({}); - - expect(result).toBe('http://localhost:3000'); + await expect(resolveBaseUrl({})).rejects.toThrow( + 'Unable to resolve base URL for workflow queue.' + ); }); - it('should handle undefined config', async () => { + it('should throw error when all resolution methods fail', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(undefined); delete process.env.PORT; - const result = await resolveBaseUrl({}); - - expect(result).toBe('http://localhost:3000'); + await expect(resolveBaseUrl({})).rejects.toThrow( + 'Unable to resolve base URL for workflow queue.' + ); }); - it('should handle config with only dataDir', async () => { + it('should handle config with only dataDir and use PORT env var', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(5173); + process.env.PORT = '4000'; const result = await resolveBaseUrl({ dataDir: './custom-data', }); - expect(result).toBe('http://localhost:5173'); + expect(result).toBe('http://localhost:4000'); + expect(getPort).not.toHaveBeenCalled(); }); - it('should skip null port and use auto-detection', async () => { + it('should skip null port and use PORT env var or auto-detection', async () => { const { getPort } = await import('@workflow/utils/get-port'); vi.mocked(getPort).mockResolvedValue(5173); + delete process.env.PORT; const result = await resolveBaseUrl({ port: null as any, @@ -269,5 +275,15 @@ describe('resolveBaseUrl', () => { expect(result).toBe('http://localhost:5173'); expect(getPort).toHaveBeenCalled(); }); + + it('should provide helpful error message when no URL can be resolved', async () => { + const { getPort } = await import('@workflow/utils/get-port'); + vi.mocked(getPort).mockResolvedValue(undefined); + delete process.env.PORT; + + await expect(resolveBaseUrl({})).rejects.toThrow( + 'Unable to resolve base URL for workflow queue.' + ); + }); }); }); diff --git a/packages/world-local/src/config.ts b/packages/world-local/src/config.ts index e36264d5e..732843f1e 100644 --- a/packages/world-local/src/config.ts +++ b/packages/world-local/src/config.ts @@ -28,9 +28,8 @@ export const config = once(() => { * Resolves the base URL for queue requests following the priority order: * 1. config.baseUrl (highest priority - full override from args or WORKFLOW_EMBEDDED_BASE_URL env var) * 2. config.port (explicit port override from args) - * 3. Auto-detected port via pid-port (primary approach) - * 4. PORT env var (fallback) - * 5. Fallback to 3000 + * 3. PORT env var (explicit configuration) + * 4. Auto-detected port via getPort (detect actual listening port) */ export async function resolveBaseUrl(config: Partial): Promise { if (config.baseUrl) { @@ -41,14 +40,14 @@ export async function resolveBaseUrl(config: Partial): Promise { return `http://localhost:${config.port}`; } + if (process.env.PORT) { + return `http://localhost:${process.env.PORT}`; + } + const detectedPort = await getPort(); if (detectedPort) { return `http://localhost:${detectedPort}`; } - if (process.env.PORT) { - return `http://localhost:${process.env.PORT}`; - } - - return 'http://localhost:3000'; + throw new Error('Unable to resolve base URL for workflow queue.'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0573c33db..78e318255 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -628,7 +628,7 @@ importers: devDependencies: '@nuxt/module-builder': specifier: 1.0.2 - version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) + version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) '@nuxt/schema': specifier: 4.2.0 version: 4.2.0 @@ -707,12 +707,12 @@ importers: packages/utils: dependencies: + execa: + specifier: 9.6.0 + version: 9.6.0 ms: specifier: 2.1.3 version: 2.1.3 - pid-port: - specifier: 2.0.0 - version: 2.0.0 devDependencies: '@types/ms': specifier: 2.1.0 @@ -10369,10 +10369,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pid-port@2.0.0: - resolution: {integrity: sha512-EDmfRxLl6lkhPjDI+19l5pkII89xVsiCP3aGjS808f7M16DyCKSXEWthD/hjyDLn5I4gKqTVw7hSgdvdXRJDTw==} - engines: {node: '>=20'} - pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -14587,7 +14583,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))': + '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))': dependencies: '@nuxt/cli': 3.29.3(magicast@0.3.5) citty: 0.1.6 @@ -14595,14 +14591,14 @@ snapshots: defu: 6.1.4 jiti: 2.6.1 magic-regexp: 0.10.0 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 tsconfck: 3.1.6(typescript@5.9.3) typescript: 5.9.3 - unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)) transitivePeerDependencies: - '@vue/compiler-core' - esbuild @@ -18025,14 +18021,6 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -21795,7 +21783,7 @@ snapshots: mkdirp@3.0.1: {} - mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): + mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): dependencies: autoprefixer: 10.4.21(postcss@8.5.6) citty: 0.1.6 @@ -21813,7 +21801,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)) mlly@1.8.0: dependencies: @@ -23107,10 +23095,6 @@ snapshots: picomatch@4.0.3: {} - pid-port@2.0.0: - dependencies: - execa: 9.6.0 - pidtree@0.6.0: {} pify@4.0.1: {} @@ -24810,7 +24794,7 @@ snapshots: ultrahtml@1.6.0: {} - unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): + unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.53.2) '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.2) @@ -24826,7 +24810,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 magic-string: 0.30.21 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 @@ -25378,7 +25362,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -25488,11 +25472,11 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)): + vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.12)(vue@3.5.22(typescript@5.9.3)): dependencies: '@babel/parser': 7.28.5 '@vue/compiler-core': 3.5.22 - esbuild: 0.25.11 + esbuild: 0.25.12 vue: 3.5.22(typescript@5.9.3) vue@3.5.22(typescript@5.9.3): diff --git a/workbench/express/nitro.config.ts b/workbench/express/nitro.config.ts index c744ea478..8e89eea15 100644 --- a/workbench/express/nitro.config.ts +++ b/workbench/express/nitro.config.ts @@ -6,4 +6,5 @@ export default defineNitroConfig({ routes: { '/**': './src/index.ts', }, + plugins: ['plugins/start-pg-world.ts'], }); diff --git a/workbench/express/plugins b/workbench/express/plugins new file mode 120000 index 000000000..2c8250f9c --- /dev/null +++ b/workbench/express/plugins @@ -0,0 +1 @@ +../nitro-v3/plugins \ No newline at end of file diff --git a/workbench/hono/nitro.config.ts b/workbench/hono/nitro.config.ts index f27eceda3..52af50f49 100644 --- a/workbench/hono/nitro.config.ts +++ b/workbench/hono/nitro.config.ts @@ -5,4 +5,5 @@ export default defineConfig({ routes: { '/**': './src/index.ts', }, + plugins: ['plugins/start-pg-world.ts'], }); diff --git a/workbench/hono/plugins b/workbench/hono/plugins new file mode 120000 index 000000000..2c8250f9c --- /dev/null +++ b/workbench/hono/plugins @@ -0,0 +1 @@ +../nitro-v3/plugins \ No newline at end of file diff --git a/workbench/nitro-v3/nitro.config.ts b/workbench/nitro-v3/nitro.config.ts index 5e800f3a1..946bbbb53 100644 --- a/workbench/nitro-v3/nitro.config.ts +++ b/workbench/nitro-v3/nitro.config.ts @@ -3,4 +3,5 @@ import { defineConfig } from 'nitro'; export default defineConfig({ modules: ['workflow/nitro'], serverDir: './', + plugins: ['plugins/start-pg-world.ts'], }); diff --git a/workbench/nitro-v3/plugins/start-pg-world.ts b/workbench/nitro-v3/plugins/start-pg-world.ts new file mode 100644 index 000000000..7e9cff224 --- /dev/null +++ b/workbench/nitro-v3/plugins/start-pg-world.ts @@ -0,0 +1,12 @@ +import { defineNitroPlugin } from 'nitro/~internal/runtime/plugin'; + +// Start the Postgres World +// Needed since we test this in CI +export default defineNitroPlugin(async () => { + if (process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres') { + import('workflow/runtime').then(async ({ getWorld }) => { + console.log('Starting Postgres World...'); + await getWorld().start?.(); + }); + } +}); diff --git a/workbench/nuxt/server/plugins/start-pg-world.ts b/workbench/nuxt/server/plugins/start-pg-world.ts new file mode 100644 index 000000000..2824d2b3e --- /dev/null +++ b/workbench/nuxt/server/plugins/start-pg-world.ts @@ -0,0 +1,12 @@ +import { defineNitroPlugin } from '#imports'; + +// Start the Postgres World +// Needed since we test this in CI +export default defineNitroPlugin(async () => { + if (process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres') { + import('workflow/runtime').then(async ({ getWorld }) => { + console.log('Starting Postgres World...'); + await getWorld().start?.(); + }); + } +}); diff --git a/workbench/sveltekit/src/hooks.server.ts b/workbench/sveltekit/src/hooks.server.ts new file mode 100644 index 000000000..16d598cf0 --- /dev/null +++ b/workbench/sveltekit/src/hooks.server.ts @@ -0,0 +1,11 @@ +import type { ServerInit } from '@sveltejs/kit'; + +export const init: ServerInit = async () => { + // Start the Postgres World + // Needed since we test this in CI + if (process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres') { + const { getWorld } = await import('workflow/runtime'); + console.log('Starting Postgres World...'); + await getWorld().start?.(); + } +}; diff --git a/workbench/vite/plugins b/workbench/vite/plugins new file mode 120000 index 000000000..2c8250f9c --- /dev/null +++ b/workbench/vite/plugins @@ -0,0 +1 @@ +../nitro-v3/plugins \ No newline at end of file diff --git a/workbench/vite/vite.config.ts b/workbench/vite/vite.config.ts index a892fda5e..3ec767479 100644 --- a/workbench/vite/vite.config.ts +++ b/workbench/vite/vite.config.ts @@ -6,5 +6,6 @@ export default defineConfig({ plugins: [nitro(), workflow()], nitro: { serverDir: './', + plugins: ['plugins/start-pg-world.ts'], }, });