Skip to content

Commit 57f6033

Browse files
committed
feat: enhance development experience with improved scripts and port conflict handling
1 parent 66441ed commit 57f6033

4 files changed

Lines changed: 136 additions & 4 deletions

File tree

fe/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ pnpm generate:app
6666
pnpm generate:sdk
6767
```
6868

69+
`pnpm dev` 先构建依赖,再统一启动代理和 Vite;按 Ctrl+C 会同时停止两个服务。
70+
开发端口 5173 被占用时会直接报错并停止本次启动的代理,不会自动切换端口。
71+
`pnpm test:scripts` 包含真实开发进程的退出、重启和端口冲突回归;Windows 或本机开发端口正在使用时跳过这组进程测试。
72+
6973
### 资源生成工具
7074

7175
#### pnpm compile

fe/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
"build:dev": "pnpm --filter container... build --mode development",
1313
"build": "pnpm --filter container... build --mode production",
1414
"compile": "node ./packages/compiler/src/bin/compile.js",
15-
"dev": "concurrently --kill-others-on-fail \"pnpm start:proxy\" \"pnpm start:dev\"",
15+
"dev": "pnpm build:dev-deps && pnpm dev:services",
16+
"build:dev-deps": "pnpm --filter @dimina/fe-container-sdk^... build --mode development",
17+
"dev:services": "concurrently --kill-others \"node ./packages/server/index.js\" \"vite packages/container --mode development\"",
1618
"dev:native": "pnpm --filter container serve",
17-
"start:proxy": "pnpm --filter server dev",
18-
"start:dev": "pnpm --filter @dimina/fe-container-sdk^... build --mode development && pnpm --filter @dimina/container dev --mode development",
19+
"start:proxy": "node ./packages/server/index.js",
20+
"start:dev": "pnpm build:dev-deps && vite packages/container --mode development",
1921
"preview": "concurrently --kill-others-on-fail \"pnpm start:proxy\" \"pnpm preview:container\"",
2022
"preview:container": "pnpm --dir ./packages/container preview",
2123
"generate:sdk": "node ./scripts/generate-sdk.js",
2224
"generate:app": "node ./scripts/generate-app.js",
2325
"test": "pnpm --filter compiler --filter common --filter components --filter service --filter render --filter container --filter fe-container-sdk test && pnpm test:scripts",
24-
"test:scripts": "node --test scripts/generate-app.test.js",
26+
"test:scripts": "node --test scripts/generate-app.test.js scripts/dev.test.js",
2527
"coverage": "pnpm --filter compiler --filter common --filter components --filter service --filter render --filter container --filter fe-container-sdk coverage",
2628
"lint": "oxlint",
2729
"lint:fix": "oxlint --fix"

fe/packages/container/vite.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default defineConfig(({ command, mode }) => {
2020
return {
2121
base: process.env.GITHUB_ACTIONS ? '/dimina/' : '/',
2222
server: {
23+
strictPort: true,
2324
open: true, // 启动后是否自动打开浏览器
2425
},
2526
define: {

fe/scripts/dev.test.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const assert = require('node:assert/strict')
2+
const { spawn, execFileSync } = require('node:child_process')
3+
const { once } = require('node:events')
4+
const net = require('node:net')
5+
const path = require('node:path')
6+
const { setTimeout: delay } = require('node:timers/promises')
7+
const { test } = require('node:test')
8+
9+
const feRoot = path.resolve(__dirname, '..')
10+
const ports = [[5173, 'localhost'], [7788, '127.0.0.1']]
11+
12+
async function bind(port, host) {
13+
const server = net.createServer()
14+
server.listen(port, host)
15+
await once(server, 'listening')
16+
return server
17+
}
18+
19+
async function portsAreFree(entries = ports) {
20+
const held = []
21+
try {
22+
for (const [port, host] of entries) held.push(await bind(port, host))
23+
return true
24+
}
25+
catch { return false }
26+
finally { for (const server of held) server.close() }
27+
}
28+
29+
async function until(check, message, timeout = 15000) {
30+
const deadline = Date.now() + timeout
31+
while (!await check()) {
32+
assert.ok(Date.now() < deadline, message())
33+
await delay(25)
34+
}
35+
}
36+
37+
function descendants(pid) {
38+
const rows = execFileSync('ps', ['-eo', 'pid=,ppid='], { encoding: 'utf8' })
39+
.trim().split('\n').map(line => line.trim().split(/\s+/).map(Number))
40+
const found = new Set([pid])
41+
let changed = true
42+
while (changed) {
43+
changed = false
44+
for (const [child, parent] of rows) {
45+
if (found.has(parent) && !found.has(child)) { found.add(child); changed = true }
46+
}
47+
}
48+
return [...found]
49+
}
50+
51+
function signal(pid, name) {
52+
try { process.kill(pid, name) }
53+
catch (error) { if (error.code !== 'ESRCH') throw error }
54+
}
55+
56+
function launch(script = 'dev') {
57+
const pnpm = process.env.npm_execpath || 'pnpm'
58+
const nodeEntry = /\.[cm]?js$/.test(pnpm)
59+
const child = spawn(nodeEntry ? process.execPath : pnpm,
60+
nodeEntry ? [pnpm, 'run', script] : ['run', script], {
61+
cwd: feRoot, detached: true, env: { ...process.env, BROWSER: 'none', NO_COLOR: '1' },
62+
stdio: ['ignore', 'pipe', 'pipe'],
63+
})
64+
let output = ''
65+
let exited = false
66+
let code
67+
let owned = []
68+
child.stdout.on('data', data => { output += data })
69+
child.stderr.on('data', data => { output += data })
70+
child.on('exit', value => { exited = true; code = value })
71+
child.on('error', error => { output += error.message; exited = true; code = -1 })
72+
return {
73+
async ready() {
74+
await until(() => exited || (output.includes('Local:') && output.includes('Dimina proxy is listening')),
75+
() => `Development services did not start:\n${output}`)
76+
assert.equal(exited, false, output)
77+
owned = descendants(child.pid)
78+
},
79+
async stop(name) {
80+
signal(-child.pid, name)
81+
await until(() => exited, () => `Development command did not exit:\n${output}`)
82+
await until(portsAreFree, () => `Development services retained their ports after ${name}:\n${output}`, 3000)
83+
},
84+
async fails() {
85+
await until(() => {
86+
if (!exited && child.pid) owned = [...new Set([...owned, ...descendants(child.pid)])]
87+
assert.doesNotMatch(output, /trying another one/)
88+
return exited
89+
}, () => `Conflicting development server did not exit:\n${output}`)
90+
assert.notEqual(code, 0, output)
91+
assert.match(output, /Port 5173 is already in use/)
92+
assert.doesNotMatch(output, /trying another one/)
93+
},
94+
cleanup() {
95+
// Only this test's recorded process tree, never unrelated port owners.
96+
for (const pid of owned.reverse()) signal(pid, 'SIGTERM')
97+
if (!exited && child.pid) signal(-child.pid, 'SIGTERM')
98+
child.stdout.destroy()
99+
child.stderr.destroy()
100+
},
101+
}
102+
}
103+
104+
test('development services release their ports and reject conflicting launches', {
105+
skip: process.platform === 'win32' ? 'POSIX process group signals' : false,
106+
timeout: 60000,
107+
}, async (t) => {
108+
if (!await portsAreFree()) { t.skip('Development ports are already occupied'); return }
109+
for (const name of ['SIGINT', 'SIGTERM']) {
110+
await t.test(`pnpm dev can stop with ${name} and restart`, async () => {
111+
const app = launch()
112+
try { await app.ready(); await app.stop(name) }
113+
finally { app.cleanup() }
114+
})
115+
}
116+
await t.test('a Vite port conflict also shuts down the proxy', async () => {
117+
const blocker = await bind(5173, 'localhost')
118+
const app = launch('dev:services')
119+
try {
120+
await app.fails()
121+
await until(() => portsAreFree([ports[1]]), () => 'The proxy outlived the failed Vite startup', 3000)
122+
}
123+
finally { app.cleanup(); await new Promise(resolve => blocker.close(resolve)) }
124+
})
125+
})

0 commit comments

Comments
 (0)