@stacksjs/rpx 0.11.2 → 0.11.4

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/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { startProxies as startProxiesFunc } from './start'
2
+
3
+ export { colors } from './colors'
4
+
5
+ export { config, config as defaultConfig } from './config'
6
+
7
+ export {
8
+ addHosts,
9
+ checkHosts,
10
+ removeHosts,
11
+ } from './hosts'
12
+
13
+ export {
14
+ checkExistingCertificates,
15
+ cleanupCertificates,
16
+ forceTrustCertificate,
17
+ generateCertificate,
18
+ httpsConfig,
19
+ isCertTrusted,
20
+ loadSSLConfig,
21
+ } from './https'
22
+
23
+ export { DefaultPortManager, findAvailablePort, isPortInUse, portManager } from './port-manager'
24
+
25
+ export { cleanup } from './start'
26
+
27
+ export { startProxies, startProxy, startServer } from './start'
28
+
29
+ export * from './types'
30
+
31
+ export * from './utils'
32
+
33
+ export default startProxiesFunc
package/src/logger.ts ADDED
@@ -0,0 +1,19 @@
1
+ export const log: {
2
+ info: (...args: any[]) => void
3
+ success: (...args: any[]) => void
4
+ warn: (...args: any[]) => void
5
+ error: (...args: any[]) => void
6
+ debug: (...args: any[]) => void
7
+ log: (...args: any[]) => void
8
+ start: (...args: any[]) => void
9
+ box: (...args: any[]) => void
10
+ } = {
11
+ info: (...args: any[]) => console.log('[info]', ...args),
12
+ success: (...args: any[]) => console.log('[success]', ...args),
13
+ warn: (...args: any[]) => console.warn('[warn]', ...args),
14
+ error: (...args: any[]) => console.error('[error]', ...args),
15
+ debug: (...args: any[]) => console.debug('[debug]', ...args),
16
+ log: (...args: any[]) => console.log(...args),
17
+ start: (...args: any[]) => console.log('[start]', ...args),
18
+ box: (...args: any[]) => console.log('[box]', ...args),
19
+ }
@@ -0,0 +1,183 @@
1
+ import type { PortManager } from './types'
2
+ import * as net from 'node:net'
3
+ import { debugLog } from './utils'
4
+
5
+ /**
6
+ * Check if a port is in use
7
+ */
8
+ export function isPortInUse(port: number, hostname: string, verbose?: boolean): Promise<boolean> {
9
+ debugLog('port', `Checking if port ${port} is in use on ${hostname}`, verbose)
10
+ return new Promise((resolve) => {
11
+ const server = net.createServer()
12
+
13
+ // Add a timeout to ensure we don't hang indefinitely
14
+ const timeout = setTimeout(() => {
15
+ debugLog('port', `Checking port ${port} timed out, assuming it's in use`, verbose)
16
+ server.close()
17
+ resolve(true)
18
+ }, 3000) // 3 second timeout
19
+
20
+ server.once('error', (err: NodeJS.ErrnoException) => {
21
+ clearTimeout(timeout)
22
+ if (err.code === 'EADDRINUSE') {
23
+ debugLog('port', `Port ${port} is in use`, verbose)
24
+ resolve(true)
25
+ }
26
+ else {
27
+ // Other errors should also be treated as port unavailable
28
+ debugLog('port', `Error checking port ${port}: ${err.message}`, verbose)
29
+ resolve(true)
30
+ }
31
+ })
32
+
33
+ server.once('listening', () => {
34
+ clearTimeout(timeout)
35
+ debugLog('port', `Port ${port} is available`, verbose)
36
+ server.close()
37
+ resolve(false)
38
+ })
39
+
40
+ try {
41
+ server.listen(port, hostname)
42
+ }
43
+ catch (err) {
44
+ clearTimeout(timeout)
45
+ debugLog('port', `Exception checking port ${port}: ${err}`, verbose)
46
+ resolve(true)
47
+ }
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Find next available port
53
+ */
54
+ export async function findAvailablePort(
55
+ startPort: number,
56
+ hostname: string,
57
+ verbose?: boolean,
58
+ maxAttempts = 50,
59
+ ): Promise<number> {
60
+ debugLog('port', `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose)
61
+ let port = startPort
62
+ let attempts = 0
63
+
64
+ while (attempts < maxAttempts) {
65
+ attempts++
66
+ const isInUse = await isPortInUse(port, hostname, verbose)
67
+
68
+ if (!isInUse) {
69
+ debugLog('port', `Found available port: ${port} after ${attempts} attempts`, verbose)
70
+ return port
71
+ }
72
+
73
+ debugLog('port', `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose)
74
+ port++
75
+ }
76
+
77
+ throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`)
78
+ }
79
+
80
+ /**
81
+ * Test if a port is actually connectable
82
+ */
83
+ export function testPortConnectivity(
84
+ port: number,
85
+ hostname: string,
86
+ timeout = 5000,
87
+ verbose?: boolean,
88
+ ): Promise<boolean> {
89
+ debugLog('port', `Testing connection to ${hostname}:${port}`, verbose)
90
+ return new Promise((resolve) => {
91
+ const socket = net.connect({
92
+ host: hostname,
93
+ port,
94
+ timeout,
95
+ })
96
+
97
+ socket.once('connect', () => {
98
+ debugLog('port', `Successfully connected to ${hostname}:${port}`, verbose)
99
+ socket.end()
100
+ resolve(true)
101
+ })
102
+
103
+ socket.once('timeout', () => {
104
+ debugLog('port', `Connection to ${hostname}:${port} timed out`, verbose)
105
+ socket.destroy()
106
+ resolve(false)
107
+ })
108
+
109
+ socket.once('error', (err) => {
110
+ debugLog('port', `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose)
111
+ socket.destroy()
112
+ resolve(false)
113
+ })
114
+ })
115
+ }
116
+
117
+ export class DefaultPortManager implements PortManager {
118
+ usedPorts: Set<number> = new Set()
119
+ private hostname: string
120
+ private verbose?: boolean
121
+ private maxRetries: number
122
+
123
+ constructor(hostname: string = '0.0.0.0', verbose?: boolean, maxRetries = 50) {
124
+ this.hostname = hostname
125
+ this.verbose = verbose
126
+ this.maxRetries = maxRetries
127
+ }
128
+
129
+ async getNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
130
+ if (this.usedPorts.has(startPort)) {
131
+ // If we already have this port registered as used, find another one
132
+ return this.findNextAvailablePort(startPort + 1, testConnectivity)
133
+ }
134
+
135
+ const isInUse = await isPortInUse(startPort, this.hostname, this.verbose)
136
+
137
+ if (isInUse) {
138
+ return this.findNextAvailablePort(startPort + 1, testConnectivity)
139
+ }
140
+
141
+ // If requested, test that we can actually connect to this port
142
+ if (testConnectivity) {
143
+ const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose)
144
+ if (!isConnectable) {
145
+ debugLog('port', `Port ${startPort} is available but not connectable, trying next port`, this.verbose)
146
+ return this.findNextAvailablePort(startPort + 1, testConnectivity)
147
+ }
148
+ }
149
+
150
+ // Port is available, register it
151
+ this.usedPorts.add(startPort)
152
+ return startPort
153
+ }
154
+
155
+ private async findNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
156
+ const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries)
157
+
158
+ // If requested, test that we can actually connect to this port
159
+ if (testConnectivity) {
160
+ const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose)
161
+ if (!isConnectable) {
162
+ // If the port isn't connectable, try the next one
163
+ if (port < startPort + this.maxRetries) {
164
+ return this.findNextAvailablePort(port + 1, testConnectivity)
165
+ }
166
+ else {
167
+ throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)
168
+ }
169
+ }
170
+ }
171
+
172
+ this.usedPorts.add(port)
173
+ return port
174
+ }
175
+
176
+ releasePort(port: number): void {
177
+ debugLog('port', `Releasing port ${port}`, this.verbose)
178
+ this.usedPorts.delete(port)
179
+ }
180
+ }
181
+
182
+ // Global port manager instance
183
+ export const portManager: DefaultPortManager = new DefaultPortManager()
@@ -0,0 +1,164 @@
1
+ import type { ChildProcess } from 'node:child_process'
2
+ import type { StartOptions } from './types'
3
+ import { spawn } from 'node:child_process'
4
+ import * as process from 'node:process'
5
+ import { log } from './logger'
6
+ import { debugLog } from './utils'
7
+
8
+ export interface ManagedProcess {
9
+ command: string
10
+ cwd: string
11
+ process: ChildProcess | null
12
+ env?: Record<string, string>
13
+ }
14
+
15
+ export class ProcessManager {
16
+ private processes: Map<string, ManagedProcess> = new Map()
17
+ private isShuttingDown = false
18
+
19
+ async startProcess(id: string, options: StartOptions, verbose?: boolean): Promise<void> {
20
+ if (this.processes.has(id)) {
21
+ debugLog('start', `Process ${id} is already running`, verbose)
22
+ return
23
+ }
24
+
25
+ const [cmd, ...args] = options.command.split(' ')
26
+ const cwd = options.cwd || process.cwd()
27
+
28
+ debugLog('start', `Starting process ${id}:`, verbose)
29
+ debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
30
+ debugLog('start', ` Working directory: ${cwd}`, verbose)
31
+ debugLog('start', ` Environment variables: ${JSON.stringify(options.env)}`, verbose)
32
+
33
+ const childProcess = spawn(cmd, args, {
34
+ cwd,
35
+ env: { ...process.env, ...options.env },
36
+ shell: true,
37
+ stdio: 'inherit',
38
+ })
39
+
40
+ this.processes.set(id, {
41
+ command: options.command,
42
+ cwd,
43
+ process: childProcess,
44
+ env: options.env,
45
+ })
46
+
47
+ return new Promise((resolve, reject) => {
48
+ childProcess.on('error', (err) => {
49
+ if (!this.isShuttingDown) {
50
+ debugLog('start', `Process ${id} failed to start: ${err}`, verbose)
51
+ this.processes.delete(id)
52
+ reject(err)
53
+ // Trigger cleanup on process error
54
+ process.emit('SIGINT')
55
+ }
56
+ })
57
+
58
+ childProcess.on('exit', (code) => {
59
+ if (!this.isShuttingDown && code !== null && code !== 0) {
60
+ debugLog('start', `Process ${id} exited with code ${code}`, verbose)
61
+ this.processes.delete(id)
62
+ reject(new Error(`Process ${id} exited with code ${code}`))
63
+ // Trigger cleanup on non-zero exit
64
+ process.emit('SIGINT')
65
+ }
66
+ })
67
+
68
+ if (verbose) {
69
+ childProcess.stdout?.on('data', (data) => {
70
+ debugLog('process', `[${id}] ${data.toString().trim()}`, true)
71
+ })
72
+
73
+ childProcess.stderr?.on('data', (data) => {
74
+ debugLog('process', `[${id}] ERR: ${data.toString().trim()}`, true)
75
+ })
76
+ }
77
+
78
+ // Resolve after a delay to allow the process to start
79
+ setTimeout(() => {
80
+ if (!this.isShuttingDown && childProcess.killed) {
81
+ this.processes.delete(id)
82
+ reject(new Error(`Process ${id} was killed during startup`))
83
+ }
84
+ else {
85
+ debugLog('start', `Process ${id} started successfully`, verbose)
86
+ resolve()
87
+ }
88
+ }, 1000)
89
+ })
90
+ }
91
+
92
+ async stopProcess(id: string, verbose?: boolean): Promise<void> {
93
+ const managed = this.processes.get(id)
94
+ if (!managed?.process) {
95
+ debugLog('start', `No process found for ${id}`, verbose)
96
+ return
97
+ }
98
+
99
+ debugLog('start', `Stopping process ${id}`, verbose)
100
+
101
+ return new Promise((resolve) => {
102
+ if (!managed.process) {
103
+ resolve()
104
+ return
105
+ }
106
+
107
+ managed.process.once('exit', () => {
108
+ this.processes.delete(id)
109
+ debugLog('start', `Process ${id} stopped`, verbose)
110
+ resolve()
111
+ })
112
+
113
+ try {
114
+ managed.process.kill('SIGTERM')
115
+
116
+ // Force kill after 3 seconds if process hasn't exited
117
+ setTimeout(() => {
118
+ if (managed.process) {
119
+ debugLog('start', `Force killing process ${id}`, verbose)
120
+ try {
121
+ managed.process.kill('SIGKILL')
122
+ }
123
+ // eslint-disable-next-line unused-imports/no-unused-vars
124
+ catch (err) {
125
+ // Ignore errors during force kill
126
+ }
127
+ }
128
+ }, 3000)
129
+ }
130
+ catch (err) {
131
+ debugLog('start', `Error stopping process ${id}: ${err}`, verbose)
132
+ this.processes.delete(id)
133
+ resolve()
134
+ }
135
+ })
136
+ }
137
+
138
+ async stopAll(verbose?: boolean): Promise<void> {
139
+ if (this.isShuttingDown) {
140
+ debugLog('start', 'Already shutting down, skipping duplicate stopAll call', verbose)
141
+ return
142
+ }
143
+
144
+ this.isShuttingDown = true
145
+ debugLog('start', 'Stopping all processes', verbose)
146
+
147
+ const promises = Array.from(this.processes.keys()).map(id =>
148
+ this.stopProcess(id, verbose).catch((err) => {
149
+ log.error(`Failed to stop process ${id}:`, err)
150
+ }),
151
+ )
152
+
153
+ await Promise.allSettled(promises)
154
+ this.processes.clear()
155
+ this.isShuttingDown = false
156
+ }
157
+
158
+ isRunning(id: string): boolean {
159
+ const managed = this.processes.get(id)
160
+ return !!managed?.process && !managed.process.killed
161
+ }
162
+ }
163
+
164
+ export const processManager: ProcessManager = new ProcessManager()