Skip to content

Commit dd4cbdd

Browse files
committed
feat: add bun plugin
1 parent db0d5be commit dd4cbdd

File tree

2 files changed

+182
-6
lines changed

2 files changed

+182
-6
lines changed

docs/.vitepress/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { HeadConfig } from 'vitepress'
22
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'
33
import { withPwa } from '@vite-pwa/vitepress'
44
import { defineConfig } from 'vitepress'
5-
65
import vite from './vite.config'
76

87
// https://vitepress.dev/reference/site-config

packages/bun-plugin/src/index.ts

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,186 @@
1-
import type { BunPlugin } from 'bun'
1+
import type { BunPlugin, ServeOptions } from 'bun'
2+
import { spawn } from 'node:child_process'
3+
import path from 'node:path'
4+
import process from 'node:process'
25

3-
export const plugin: BunPlugin = {
4-
name: 'bun-plugin-rpx',
5-
async setup(_build) {
6-
},
6+
interface RpxPluginOptions {
7+
/**
8+
* The domain to use instead of localhost:port
9+
* @example 'my-app.test', 'awesome.localhost'
10+
* @default '$projectName.localhost'
11+
*/
12+
domain?: string
13+
14+
/**
15+
* Allow HTTPS
16+
* @default true
17+
*/
18+
https?: boolean
19+
20+
/**
21+
* Enable debug logging
22+
* @default false
23+
*/
24+
verbose?: boolean
25+
}
26+
27+
interface ServeFunction {
28+
(options?: ServeOptions): {
29+
start: (...args: unknown[]) => Promise<{ port: number }>
30+
stop: () => Promise<void>
31+
}
32+
}
33+
34+
interface PluginBuilder {
35+
serve: ServeFunction
36+
}
37+
38+
const defaultOptions: Required<RpxPluginOptions> = {
39+
domain: '',
40+
https: true,
41+
verbose: false,
42+
}
43+
44+
/**
45+
* A Bun plugin to provide custom domain names for local development
46+
* instead of using localhost:port
47+
*/
48+
export function plugin(options: RpxPluginOptions = {}): BunPlugin {
49+
const pluginOpts: Required<RpxPluginOptions> = { ...defaultOptions, ...options }
50+
51+
// Store server instance and port to clean up later
52+
let serverPort: number | null = null
53+
let rpxProcess: ReturnType<typeof spawn> | null = null
54+
55+
return {
56+
name: 'bun-plugin-rpx',
57+
async setup(build) {
58+
// Get the project name from package.json as a fallback domain
59+
let projectName = ''
60+
try {
61+
const pkgPath = path.join(process.cwd(), 'package.json')
62+
const pkg = await import(pkgPath, {
63+
with: { type: 'json' },
64+
})
65+
projectName = pkg.default.name || 'app'
66+
}
67+
catch {
68+
projectName = 'app'
69+
}
70+
71+
// Use provided domain or fallback to projectName.localhost
72+
const domain = pluginOpts.domain || `${projectName}.localhost`
73+
74+
// Hook into serve to intercept port and start rpx
75+
const buildWithServe = build as unknown as PluginBuilder
76+
const originalServe = buildWithServe.serve
77+
78+
buildWithServe.serve = (options?: ServeOptions) => {
79+
// Store the original serve function result
80+
const server = originalServe(options)
81+
const originalStart = server.start
82+
83+
server.start = async (...args: unknown[]) => {
84+
// Start the original server
85+
const result = await originalStart.apply(server, args)
86+
87+
// Get the port from the server
88+
serverPort = result.port
89+
90+
if (serverPort) {
91+
await startRpx(domain, serverPort, pluginOpts.https, pluginOpts.verbose)
92+
}
93+
94+
return result
95+
}
96+
97+
// Handle server stop to clean up rpx
98+
const originalStop = server.stop
99+
server.stop = async () => {
100+
if (rpxProcess) {
101+
rpxProcess.kill()
102+
rpxProcess = null
103+
}
104+
105+
return originalStop.apply(server)
106+
}
107+
108+
return server
109+
}
110+
111+
// Handle build process exit
112+
process.on('exit', cleanup)
113+
process.on('SIGINT', () => {
114+
cleanup()
115+
process.exit(0)
116+
})
117+
process.on('SIGTERM', () => {
118+
cleanup()
119+
process.exit(0)
120+
})
121+
},
122+
}
123+
124+
/**
125+
* Start rpx process
126+
*/
127+
async function startRpx(domain: string, port: number, https: boolean, verbose: boolean) {
128+
// Find rpx binary - it should be installed as a dependency
129+
try {
130+
const rpxBinary = 'rpx'
131+
132+
// Build the command to run rpx
133+
const args = [
134+
`--from=localhost:${port}`,
135+
`--to=${domain}`,
136+
]
137+
138+
// Add HTTPS flag if needed
139+
if (https) {
140+
args.push('--https')
141+
}
142+
143+
// Add verbose flag if needed
144+
if (verbose) {
145+
args.push('--verbose')
146+
}
147+
148+
// Start rpx process
149+
rpxProcess = spawn(rpxBinary, args, {
150+
stdio: verbose ? 'inherit' : 'ignore',
151+
detached: false,
152+
})
153+
154+
// Log startup information
155+
console.error(`\n🌐 rpx: ${https ? 'https' : 'http'}://${domain} -> localhost:${port}\n`)
156+
157+
// Handle process events
158+
rpxProcess.on('error', (err) => {
159+
console.error(`rpx error: ${err.message}`)
160+
rpxProcess = null
161+
})
162+
163+
rpxProcess.on('exit', (code) => {
164+
if (code !== 0 && code !== null) {
165+
console.error(`rpx exited with code ${code}`)
166+
}
167+
rpxProcess = null
168+
})
169+
}
170+
catch (error) {
171+
console.error('Failed to start rpx:', error)
172+
}
173+
}
174+
175+
/**
176+
* Clean up rpx process
177+
*/
178+
function cleanup() {
179+
if (rpxProcess) {
180+
rpxProcess.kill()
181+
rpxProcess = null
182+
}
183+
}
7184
}
8185

9186
export default plugin

0 commit comments

Comments
 (0)