@avelor/mesh 0.2.0 → 0.2.1

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/bin/mesh.js CHANGED
@@ -14,6 +14,7 @@ const STATE_FILE = '/tmp/.mesh.json'
14
14
  const RESET = '\x1b[0m'
15
15
  const DIM = '\x1b[2m'
16
16
  const GREEN = '\x1b[32m'
17
+ const YELLOW = '\x1b[33m'
17
18
  const CYAN = '\x1b[36m'
18
19
 
19
20
  // ── Argument parsing ──────────────────────────────────────────────────────────
@@ -57,9 +58,36 @@ if (cmd === 'start') {
57
58
  detached: true,
58
59
  stdio: 'ignore',
59
60
  })
60
- child.unref()
61
- console.log(`mesh: started (pid ${child.pid})`)
62
- process.exit(0)
61
+
62
+ const pid = child.pid
63
+ const deadline = Date.now() + 5000
64
+
65
+ ;(function poll() {
66
+ let alive = true
67
+ try { process.kill(pid, 0) } catch (e) { if (e.code !== 'EPERM') alive = false }
68
+
69
+ if (!alive) {
70
+ console.error('mesh: failed to start — check your mesh.yml and that ports 80/443 are free')
71
+ process.exit(1)
72
+ }
73
+
74
+ try {
75
+ const state = JSON.parse(readFileSync(STATE_FILE, 'utf8'))
76
+ if (state.pid === pid) {
77
+ child.unref()
78
+ console.log(`mesh: started (pid ${pid})`)
79
+ process.exit(0)
80
+ }
81
+ } catch { /* not ready yet */ }
82
+
83
+ if (Date.now() >= deadline) {
84
+ child.kill()
85
+ console.error('mesh: timed out waiting for proxy to start')
86
+ process.exit(1)
87
+ }
88
+
89
+ setTimeout(poll, 50)
90
+ })()
63
91
  }
64
92
 
65
93
  if (cmd === 'stop') {
@@ -120,6 +148,17 @@ if (cmd === 'status') {
120
148
  for (const [name, port] of Object.entries(state.services)) {
121
149
  console.log(` ${GREEN}${name.padEnd(pad)}.test${RESET} ${DIM}→ :${port} ${protocol}://${name}.test${RESET}`)
122
150
  }
151
+ const rules = state.rules ?? {}
152
+ if (Object.keys(rules).length) {
153
+ console.log('')
154
+ for (const [svc, ruleList] of Object.entries(rules)) {
155
+ for (const r of ruleList) {
156
+ const type = r.status ? `${r.status}` : `${r.delay}ms delay`
157
+ const methodStr = r.method ? `${r.method} ` : ''
158
+ console.log(` ${YELLOW}${svc}${r.path}${RESET} ${DIM}${methodStr}${r.rate}% → ${type}${RESET}`)
159
+ }
160
+ }
161
+ }
123
162
  console.log('')
124
163
  process.exit(0)
125
164
  }
@@ -160,7 +199,7 @@ const configDir = dirname(resolve(configPath))
160
199
  const certs = ensureCerts(services, configDir)
161
200
  const servers = startProxy(services, rules, certs)
162
201
 
163
- writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, https: !!certs }))
202
+ writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, rules, https: !!certs }))
164
203
 
165
204
  // ── Crash safety — clean /etc/hosts even on unexpected exit ───────────────────
166
205
 
@@ -202,7 +241,7 @@ watch(configPath, () => {
202
241
  Object.assign(rules, next.rules)
203
242
 
204
243
  writeHosts(services)
205
- writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, https: !!certs }))
244
+ writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, rules, https: !!certs }))
206
245
 
207
246
  if (certs && hasNewServices) {
208
247
  const newCerts = ensureCerts(services, configDir)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avelor/mesh",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local dev proxy. Named services, failure rules, no ports.",
5
5
  "keywords": [
6
6
  "proxy",
package/src/hosts.js CHANGED
@@ -8,7 +8,7 @@ export function writeHosts(services, hostsFile = HOSTS_FILE) {
8
8
  const current = readFileSync(hostsFile, 'utf8')
9
9
  const clean = removeMeshBlock(current)
10
10
  const entries = Object.keys(services)
11
- .map(name => `127.0.0.1 ${name}.test`)
11
+ .flatMap(name => [`127.0.0.1 ${name}.test`, `::1 ${name}.test`])
12
12
  .join('\n')
13
13
  const next = `${clean.trimEnd()}\n\n${MESH_START}\n${entries}\n${MESH_END}\n`
14
14
  writeFileSync(hostsFile, next, 'utf8')
package/src/proxy.js CHANGED
@@ -1,10 +1,31 @@
1
1
  import http from 'http'
2
2
  import https from 'https'
3
+ import net from 'net'
3
4
  import { readFileSync } from 'fs'
4
5
  import httpProxy from 'http-proxy'
5
6
  import { matchRule, applyRule } from './rules.js'
6
7
  import { wantsHtml, errorPage } from './error-page.js'
7
8
 
9
+ const hostCache = new Map()
10
+
11
+ function probeHost(port) {
12
+ if (hostCache.has(port)) return Promise.resolve(hostCache.get(port))
13
+ const probe = addr => new Promise((resolve, reject) => {
14
+ const s = net.connect(port, addr)
15
+ s.setTimeout(200)
16
+ s.on('connect', () => { s.destroy(); resolve(addr) })
17
+ s.on('timeout', () => { s.destroy(); reject(new Error('timeout')) })
18
+ s.on('error', reject)
19
+ })
20
+ return Promise.any([probe('127.0.0.1'), probe('::1')])
21
+ .then(host => { hostCache.set(port, host); return host })
22
+ .catch(() => '127.0.0.1')
23
+ }
24
+
25
+ function fmtHost(host) {
26
+ return host.includes(':') ? `[${host}]` : host
27
+ }
28
+
8
29
  const RESET = '\x1b[0m'
9
30
  const DIM = '\x1b[2m'
10
31
  const GREEN = '\x1b[32m'
@@ -65,14 +86,16 @@ export function startProxy(services, rules, certs = null) {
65
86
  log(YELLOW, `${rule.delay}ms`, name, pathname, `→ :${target} (delayed)`)
66
87
  }
67
88
 
68
- proxy.web(req, res, { target: `http://127.0.0.1:${target}` })
89
+ const host = await probeHost(target)
90
+ proxy.web(req, res, { target: `http://${fmtHost(host)}:${target}` })
69
91
  if (!rule) log(DIM, '→', name, pathname, `→ :${target}`)
70
92
  }
71
93
 
72
- function handleUpgrade(req, socket, head) {
94
+ async function handleUpgrade(req, socket, head) {
73
95
  const { name, target } = resolveService(req.headers.host)
74
96
  if (!target) { socket.destroy(); return }
75
- proxy.ws(req, socket, head, { target: `ws://127.0.0.1:${target}` }, err => {
97
+ const host = await probeHost(target)
98
+ proxy.ws(req, socket, head, { target: `ws://${fmtHost(host)}:${target}` }, err => {
76
99
  if (err) log(RED, 'WSE', name, req.url, `→ ${err.code ?? err.message}`)
77
100
  })
78
101
  log(DIM, 'WS', name, req.url, `→ :${target}`)
@@ -98,7 +121,7 @@ export function startProxy(services, rules, certs = null) {
98
121
  throw err
99
122
  })
100
123
 
101
- httpServer.listen(80, '127.0.0.1', () => onReady(services, rules, certs))
124
+ httpServer.listen(80, '::', () => onReady(services, rules, certs))
102
125
 
103
126
  let httpsServer = null
104
127
 
@@ -115,7 +138,7 @@ export function startProxy(services, rules, certs = null) {
115
138
  }
116
139
  throw err
117
140
  })
118
- httpsServer.listen(443, '127.0.0.1')
141
+ httpsServer.listen(443, '::')
119
142
  }
120
143
 
121
144
  return { http: httpServer, https: httpsServer }