@stacksjs/rpx 0.11.9 → 0.11.11
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/dist/bin/cli.js +166 -162
- package/dist/chunk-00z9z9an.js +156 -0
- package/dist/chunk-747af2w4.js +1 -0
- package/dist/chunk-end75nnv.js +1 -0
- package/dist/{chunk-jpf41gb9.js → chunk-zs1tyy8z.js} +2 -2
- package/dist/daemon.d.ts +4 -0
- package/dist/dns-state.d.ts +27 -0
- package/dist/dns.d.ts +43 -0
- package/dist/hosts.d.ts +2 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +6 -156
- package/package.json +1 -1
- package/src/daemon.ts +41 -2
- package/src/dns-state.ts +116 -0
- package/src/dns.ts +252 -142
- package/src/hosts.ts +26 -5
- package/src/index.ts +29 -0
- package/src/start.ts +7 -9
- package/src/utils.ts +7 -1
- package/dist/chunk-6z1nzq0x.js +0 -1
- package/dist/chunk-qcdcnadb.js +0 -1
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -28,6 +28,11 @@ import { checkExistingCertificates, generateCertificate } from './https'
|
|
|
28
28
|
import { createProxyFetchHandler } from './proxy-handler'
|
|
29
29
|
import { gcStaleEntries, getRegistryDir, isPidAlive, readAll, watchRegistry } from './registry'
|
|
30
30
|
import type { RegistryEntry } from './registry'
|
|
31
|
+
import {
|
|
32
|
+
reconcileStaleDevelopmentDns,
|
|
33
|
+
syncDevelopmentDnsFromRegistry,
|
|
34
|
+
tearDownDevelopmentDns,
|
|
35
|
+
} from './dns'
|
|
31
36
|
import { debugLog } from './utils'
|
|
32
37
|
|
|
33
38
|
export interface DaemonOptions {
|
|
@@ -236,7 +241,15 @@ export async function runDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle>
|
|
|
236
241
|
await gcStaleEntries(registryDir, verbose).catch((err) => {
|
|
237
242
|
debugLog('daemon', `initial gc failed: ${err}`, verbose)
|
|
238
243
|
})
|
|
239
|
-
|
|
244
|
+
const initialEntries = await readAll(registryDir, verbose)
|
|
245
|
+
rebuild(initialEntries)
|
|
246
|
+
|
|
247
|
+
await reconcileStaleDevelopmentDns({ rpxDir, verbose }).catch((err) => {
|
|
248
|
+
debugLog('daemon', `DNS reconcile on start failed: ${err}`, verbose)
|
|
249
|
+
})
|
|
250
|
+
await syncDevelopmentDnsFromRegistry(initialEntries, { rpxDir, verbose, ownerPid: process.pid }).catch((err) => {
|
|
251
|
+
debugLog('daemon', `DNS setup on start failed: ${err}`, verbose)
|
|
252
|
+
})
|
|
240
253
|
|
|
241
254
|
const sslConfig = await bootstrapTls(opts, registryDir)
|
|
242
255
|
|
|
@@ -280,7 +293,12 @@ export async function runDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle>
|
|
|
280
293
|
}
|
|
281
294
|
|
|
282
295
|
const watcher = watchRegistry(
|
|
283
|
-
(entries) => {
|
|
296
|
+
(entries) => {
|
|
297
|
+
rebuild(entries)
|
|
298
|
+
syncDevelopmentDnsFromRegistry(entries, { rpxDir, verbose, ownerPid: process.pid }).catch((err) => {
|
|
299
|
+
debugLog('daemon', `DNS sync on registry change failed: ${err}`, verbose)
|
|
300
|
+
})
|
|
301
|
+
},
|
|
284
302
|
{ dir: registryDir, verbose },
|
|
285
303
|
)
|
|
286
304
|
|
|
@@ -311,6 +329,9 @@ export async function runDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle>
|
|
|
311
329
|
// `stop(false)` lets in-flight requests drain before closing the listener.
|
|
312
330
|
httpsServer.stop(false)
|
|
313
331
|
httpServer?.stop(false)
|
|
332
|
+
await tearDownDevelopmentDns({ rpxDir, verbose }).catch((err) => {
|
|
333
|
+
debugLog('daemon', `DNS teardown failed: ${err}`, verbose)
|
|
334
|
+
})
|
|
314
335
|
await releaseDaemonLock(rpxDir)
|
|
315
336
|
if (verbose)
|
|
316
337
|
log.info('rpx daemon stopped')
|
|
@@ -391,6 +412,10 @@ export async function ensureDaemonRunning(opts: EnsureDaemonOptions = {}): Promi
|
|
|
391
412
|
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
392
413
|
const verbose = opts.verbose ?? false
|
|
393
414
|
|
|
415
|
+
await reconcileStaleDevelopmentDns({ rpxDir, verbose }).catch((err) => {
|
|
416
|
+
debugLog('daemon', `DNS reconcile before ensureDaemonRunning: ${err}`, verbose)
|
|
417
|
+
})
|
|
418
|
+
|
|
394
419
|
const existingPid = await readDaemonPid(rpxDir)
|
|
395
420
|
if (existingPid !== null && isPidAlive(existingPid)) {
|
|
396
421
|
debugLog('daemon', `ensureDaemonRunning: already running pid=${existingPid}`, verbose)
|
|
@@ -477,6 +502,7 @@ export async function stopDaemon(opts: StopDaemonOptions = {}): Promise<StopDaem
|
|
|
477
502
|
if (pid === null || !isPidAlive(pid)) {
|
|
478
503
|
if (pid !== null)
|
|
479
504
|
await releaseDaemonLock(rpxDir)
|
|
505
|
+
await reconcileStaleDevelopmentDns({ rpxDir, verbose }).catch(() => {})
|
|
480
506
|
return { stopped: false, pid, forced: false }
|
|
481
507
|
}
|
|
482
508
|
|
|
@@ -514,5 +540,18 @@ export async function stopDaemon(opts: StopDaemonOptions = {}): Promise<StopDaem
|
|
|
514
540
|
}
|
|
515
541
|
// SIGKILL bypasses the cleanup handler, so remove the pid file ourselves.
|
|
516
542
|
await releaseDaemonLock(rpxDir)
|
|
543
|
+
await tearDownDevelopmentDns({ rpxDir, verbose }).catch((err) => {
|
|
544
|
+
debugLog('daemon', `DNS teardown after SIGKILL: ${err}`, verbose)
|
|
545
|
+
})
|
|
517
546
|
return { stopped: true, pid, forced: true }
|
|
518
547
|
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* When the daemon is not running, ensure no stale macOS resolver overrides remain.
|
|
551
|
+
*/
|
|
552
|
+
export async function reconcileDevelopmentDnsOnIdle(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
|
|
553
|
+
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
554
|
+
if (await isDaemonRunning(rpxDir))
|
|
555
|
+
return
|
|
556
|
+
await reconcileStaleDevelopmentDns({ rpxDir, verbose: opts.verbose })
|
|
557
|
+
}
|
package/src/dns-state.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fsp from 'node:fs/promises'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const DNS_STATE_VERSION = 1 as const
|
|
6
|
+
export const RPX_DNS_STATE_FILE = 'dns-state.json'
|
|
7
|
+
|
|
8
|
+
/** Single-label /etc/resolver files created by older rpx versions (whole-TLD hijack). */
|
|
9
|
+
export const LEGACY_TLD_RESOLVER_LABELS = [
|
|
10
|
+
'com',
|
|
11
|
+
'test',
|
|
12
|
+
'dev',
|
|
13
|
+
'app',
|
|
14
|
+
'page',
|
|
15
|
+
'local',
|
|
16
|
+
'localhost',
|
|
17
|
+
'example',
|
|
18
|
+
'invalid',
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export interface DnsState {
|
|
22
|
+
version: typeof DNS_STATE_VERSION
|
|
23
|
+
/** Basenames under /etc/resolver/ (e.g. `postline.test`, not `test`). */
|
|
24
|
+
resolvers: string[]
|
|
25
|
+
domains: string[]
|
|
26
|
+
ownerPid: number | null
|
|
27
|
+
updatedAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defaultRpxDir(): string {
|
|
31
|
+
return path.join(homedir(), '.stacks', 'rpx')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getDnsStatePath(rpxDir: string = defaultRpxDir()): string {
|
|
35
|
+
return path.join(rpxDir, RPX_DNS_STATE_FILE)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function loadDnsState(rpxDir: string = defaultRpxDir()): Promise<DnsState | null> {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await fsp.readFile(getDnsStatePath(rpxDir), 'utf8')
|
|
41
|
+
const parsed = JSON.parse(raw) as Partial<DnsState>
|
|
42
|
+
if (parsed.version !== DNS_STATE_VERSION || !Array.isArray(parsed.resolvers))
|
|
43
|
+
return null
|
|
44
|
+
return {
|
|
45
|
+
version: DNS_STATE_VERSION,
|
|
46
|
+
resolvers: parsed.resolvers.filter((r): r is string => typeof r === 'string'),
|
|
47
|
+
domains: Array.isArray(parsed.domains)
|
|
48
|
+
? parsed.domains.filter((d): d is string => typeof d === 'string')
|
|
49
|
+
: [],
|
|
50
|
+
ownerPid: typeof parsed.ownerPid === 'number' ? parsed.ownerPid : null,
|
|
51
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
|
|
56
|
+
return null
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function saveDnsState(rpxDir: string, state: DnsState): Promise<void> {
|
|
62
|
+
await fsp.mkdir(rpxDir, { recursive: true })
|
|
63
|
+
await fsp.writeFile(getDnsStatePath(rpxDir), `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function clearDnsState(rpxDir: string): Promise<void> {
|
|
67
|
+
await fsp.rm(getDnsStatePath(rpxDir), { force: true })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normalize a dev hostname. Returns null for localhost / IPs — those use /etc/hosts only.
|
|
72
|
+
*/
|
|
73
|
+
export function normalizeDevDomain(raw: string): string | null {
|
|
74
|
+
const domain = raw.trim().toLowerCase().replace(/\.$/, '')
|
|
75
|
+
if (!domain || domain.includes('127.0.0.1'))
|
|
76
|
+
return null
|
|
77
|
+
if (domain === 'localhost' || domain.endsWith('.localhost'))
|
|
78
|
+
return null
|
|
79
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(domain))
|
|
80
|
+
return null
|
|
81
|
+
return domain
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* macOS resolver basename for a dev domain. Uses the registrable base (last two labels)
|
|
86
|
+
* so `api.postline.test` and `postline.test` share one `/etc/resolver/postline.test` file.
|
|
87
|
+
*/
|
|
88
|
+
export function resolverBasenameForDomain(raw: string): string | null {
|
|
89
|
+
const domain = normalizeDevDomain(raw)
|
|
90
|
+
if (!domain)
|
|
91
|
+
return null
|
|
92
|
+
const parts = domain.split('.')
|
|
93
|
+
if (parts.length < 2)
|
|
94
|
+
return null
|
|
95
|
+
return parts.slice(-2).join('.')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolverBasenamesForDomains(domains: string[]): string[] {
|
|
99
|
+
const basenames = new Set<string>()
|
|
100
|
+
for (const raw of domains) {
|
|
101
|
+
const basename = resolverBasenameForDomain(raw)
|
|
102
|
+
if (basename)
|
|
103
|
+
basenames.add(basename)
|
|
104
|
+
}
|
|
105
|
+
return Array.from(basenames).sort()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function devDomainsFromHosts(hosts: string[]): string[] {
|
|
109
|
+
const out = new Set<string>()
|
|
110
|
+
for (const raw of hosts) {
|
|
111
|
+
const domain = normalizeDevDomain(raw)
|
|
112
|
+
if (domain)
|
|
113
|
+
out.add(domain)
|
|
114
|
+
}
|
|
115
|
+
return Array.from(out).sort()
|
|
116
|
+
}
|