@ansstory/hias 1.0.4 → 1.0.5
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/.gitattributes +1 -1
- package/.vscode/settings.json +8 -0
- package/I18N.md +178 -0
- package/LICENSE +22 -22
- package/README.md +13 -11
- package/lib/config.js +22 -18
- package/lib/core/action.js +95 -68
- package/lib/core/close-port.js +173 -0
- package/lib/core/commander.js +48 -40
- package/lib/core/download.js +32 -19
- package/lib/core/help.js +17 -6
- package/lib/core/lang.js +50 -0
- package/lib/i18n/index.js +77 -0
- package/lib/i18n/resources/en.json +34 -0
- package/lib/i18n/resources/zh-CN.json +34 -0
- package/lib/i18n/store.js +85 -0
- package/lib/index.js +14 -6
- package/lib/template/component.jsx.ejs +11 -11
- package/lib/template/component.tsx.ejs +12 -12
- package/lib/template/component.vue.ejs +13 -13
- package/lib/template/reduxStore.jsx.ejs +16 -16
- package/lib/template/reduxTsStore.tsx.ejs +22 -22
- package/lib/utils/compile-ejs.js +28 -21
- package/lib/utils/write-file.js +16 -10
- package/package.json +7 -3
- package/test/close-port.test.js +88 -0
package/.gitattributes
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
* text=auto eol=lf
|
|
1
|
+
* text=auto eol=lf
|
package/I18N.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# 国际化功能文档
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
hias-cli 现在支持国际化(i18n),用户可以通过命令切换 CLI 工具的显示语言。
|
|
6
|
+
|
|
7
|
+
## 支持的语言
|
|
8
|
+
|
|
9
|
+
- **英文** (en) - 默认语言
|
|
10
|
+
- **简体中文** (zh-CN)
|
|
11
|
+
|
|
12
|
+
## 使用方法
|
|
13
|
+
|
|
14
|
+
### 1. 查看当前语言
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
hias lang -l
|
|
18
|
+
# 或
|
|
19
|
+
hias lang --list
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. 切换语言
|
|
23
|
+
|
|
24
|
+
**切换到中文:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
hias lang zh-CN
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**切换到英文:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
hias lang en
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. 语言设置的持久化
|
|
37
|
+
|
|
38
|
+
用户选择的语言会被保存到 `~/.hias-cli/config.json`,下次使用 CLI 工具时会自动使用保存的语言设置。
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"language": "zh-CN"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 技术实现
|
|
47
|
+
|
|
48
|
+
### 依赖库
|
|
49
|
+
|
|
50
|
+
- **i18next** - 专业的国际化库,用于管理多语言翻译
|
|
51
|
+
|
|
52
|
+
### 项目结构
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
lib/
|
|
56
|
+
├── i18n/
|
|
57
|
+
│ ├── index.js # i18n 初始化模块
|
|
58
|
+
│ ├── store.js # 语言配置存储和管理
|
|
59
|
+
│ └── resources/
|
|
60
|
+
│ ├── en.json # 英文翻译资源
|
|
61
|
+
│ └── zh-CN.json # 中文翻译资源
|
|
62
|
+
├── core/
|
|
63
|
+
│ ├── lang.js # 语言切换命令处理
|
|
64
|
+
│ ├── commander.js # 命令定义(已集成 i18n)
|
|
65
|
+
│ ├── action.js # 命令处理(已集成 i18n)
|
|
66
|
+
│ ├── download.js # 下载处理(已集成 i18n)
|
|
67
|
+
│ └── help.js # 帮助信息(已集成 i18n)
|
|
68
|
+
└── index.js # 入口文件(初始化 i18n)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 配置文件位置
|
|
72
|
+
|
|
73
|
+
`~/.hias-cli/config.json` - 存储用户的语言偏好
|
|
74
|
+
|
|
75
|
+
## 示例
|
|
76
|
+
|
|
77
|
+
### 场景 1:首次使用
|
|
78
|
+
|
|
79
|
+
用户首次运行 CLI 工具,默认显示英文:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ hias -h
|
|
83
|
+
Commands:
|
|
84
|
+
create|crt <project> [other...] create project. For example: hias create airbnb
|
|
85
|
+
adv <vuecpnname> [...others] add vue component into a folder...
|
|
86
|
+
...
|
|
87
|
+
lang [options] [language] set CLI language. For example: hias lang zh-CN or hias lang en
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 场景 2:切换到中文
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
$ hias lang zh-CN
|
|
94
|
+
语言已设置为: 简体中文
|
|
95
|
+
|
|
96
|
+
$ hias -h
|
|
97
|
+
Commands:
|
|
98
|
+
create|crt <project> [other...] 创建项目。示例: hias create airbnb
|
|
99
|
+
adv <vuecpnname> [...others] 将 Vue 组件添加到文件夹,示例...
|
|
100
|
+
...
|
|
101
|
+
lang [options] [language] 设置 CLI 语言。示例: hias lang zh-CN 或 hias lang en
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 场景 3:查看当前语言
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ hias lang -l
|
|
108
|
+
当前语言: 简体中文
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 场景 4:切换回英文
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
$ hias lang en
|
|
115
|
+
Language has been set to: English
|
|
116
|
+
|
|
117
|
+
$ hias lang -l
|
|
118
|
+
Current language: English
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 扩展国际化
|
|
122
|
+
|
|
123
|
+
要添加新的语言支持,只需:
|
|
124
|
+
|
|
125
|
+
1. 在 `lib/i18n/resources/` 目录中创建新的翻译文件(如 `es.json`)
|
|
126
|
+
2. 在 `lib/i18n/store.js` 中更新 `SUPPORTED_LANGUAGES` 数组
|
|
127
|
+
3. 在新的翻译文件中翻译所有的消息键
|
|
128
|
+
|
|
129
|
+
### 翻译文件结构
|
|
130
|
+
|
|
131
|
+
参考 `lib/i18n/resources/en.json` 或 `zh-CN.json`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"commands": {
|
|
136
|
+
"create": "...",
|
|
137
|
+
"adv": "...",
|
|
138
|
+
"lang": "..."
|
|
139
|
+
},
|
|
140
|
+
"options": {
|
|
141
|
+
"dest": "..."
|
|
142
|
+
},
|
|
143
|
+
"prompts": {
|
|
144
|
+
"selectTemplate": "..."
|
|
145
|
+
},
|
|
146
|
+
"messages": {
|
|
147
|
+
"downloading": "...",
|
|
148
|
+
"langSet": "...",
|
|
149
|
+
"langCurrent": "..."
|
|
150
|
+
},
|
|
151
|
+
"languages": {
|
|
152
|
+
"en": "English",
|
|
153
|
+
"zh-CN": "简体中文"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 注意事项
|
|
159
|
+
|
|
160
|
+
- 语言设置是全局的,会影响所有 CLI 命令的输出
|
|
161
|
+
- 配置文件会在用户的主目录下创建 `.hias-cli` 目录
|
|
162
|
+
- 如果配置文件损坏,CLI 会自动使用默认语言(英文)
|
|
163
|
+
- 当前支持的语言代码符合 BCP 47 标准
|
|
164
|
+
|
|
165
|
+
## 故障排除
|
|
166
|
+
|
|
167
|
+
### 问题:语言没有切换
|
|
168
|
+
|
|
169
|
+
- **解决方案**:检查 `~/.hias-cli/config.json` 是否存在且有写入权限
|
|
170
|
+
- 尝试手动删除配置文件,让 CLI 重新创建
|
|
171
|
+
|
|
172
|
+
### 问题:出现乱码
|
|
173
|
+
|
|
174
|
+
- **解决方案**:确保你的终端支持 UTF-8 编码
|
|
175
|
+
|
|
176
|
+
### 问题:某些消息仍然是英文
|
|
177
|
+
|
|
178
|
+
- **解决方案**:这可能是第三方库输出的信息,这些信息不受 i18n 控制
|
package/LICENSE
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright © 2025
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
-
of this software and associated documentation files (the “Software”), to deal
|
|
8
|
-
in the Software without restriction, including without limitation the rights
|
|
9
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
-
furnished to do so, subject to the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be included in
|
|
14
|
-
all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
-
THE SOFTWARE.
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
# hias-cli
|
|
2
|
-
```sh
|
|
3
|
-
npm i hias -g
|
|
4
|
-
hias -h
|
|
5
|
-
hias create xxx
|
|
6
|
-
hias adv Demo -d src/components
|
|
7
|
-
hias adr Demo -d src/components
|
|
8
|
-
hias adrt Demo -d src/components
|
|
9
|
-
hias adrd useDemoStore -d src/store/modules
|
|
10
|
-
hias adrdt useDemoStore -d src/store/modules
|
|
11
|
-
|
|
1
|
+
# hias-cli
|
|
2
|
+
```sh
|
|
3
|
+
npm i hias -g
|
|
4
|
+
hias -h
|
|
5
|
+
hias create xxx
|
|
6
|
+
hias adv Demo -d src/components
|
|
7
|
+
hias adr Demo -d src/components
|
|
8
|
+
hias adrt Demo -d src/components
|
|
9
|
+
hias adrd useDemoStore -d src/store/modules
|
|
10
|
+
hias adrdt useDemoStore -d src/store/modules
|
|
11
|
+
hias close-port 8076 8077
|
|
12
|
+
hias close-port 8076,8077
|
|
13
|
+
```
|
package/lib/config.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
// 项目配置文件 - 定义支持的框架模板和对应的下载地址
|
|
2
|
+
// 各框架模板的 Gitee 仓库地址映射表
|
|
3
|
+
const foramworkUrl = {
|
|
4
|
+
vue2: 'https://gitee.com/AnsStory/hias-vue2-template.git',
|
|
5
|
+
vue3: 'https://gitee.com/AnsStory/hias-vue3-template.git',
|
|
6
|
+
'vue-ts': 'https://gitee.com/AnsStory/hias-vue-ts-template.git',
|
|
7
|
+
'uni-vite': 'https://gitee.com/AnsStory/hias-uni-vite.git',
|
|
8
|
+
'nuxt-web': 'https://gitee.com/AnsStory/hias-nuxt-web-tmp.git',
|
|
9
|
+
'vue-screen': 'https://gitee.com/AnsStory/hias-vite-screen.git',
|
|
10
|
+
react: 'https://gitee.com/AnsStory/hias-react-template.git',
|
|
11
|
+
'react-ts': 'https://gitee.com/AnsStory/hias-react-ts-template.git',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 从 foramworkUrl 对象提取所有支持的框架名称
|
|
15
|
+
const framwork = Object.keys(foramworkUrl)
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
// 可选择的框架列表
|
|
19
|
+
framwork,
|
|
20
|
+
// 框架对应的下载地址映射表
|
|
21
|
+
foramworkUrl,
|
|
22
|
+
}
|
package/lib/core/action.js
CHANGED
|
@@ -1,68 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
// 命令行操作处理文件 - 处理各种命令的具体执行逻辑
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const { program } = require('commander')
|
|
6
|
+
var config = require('../config')
|
|
7
|
+
var downloadFun = require('./download')
|
|
8
|
+
const compileEjs = require('../utils/compile-ejs')
|
|
9
|
+
const writeFile = require('../utils/write-file')
|
|
10
|
+
const i18n = require('../i18n')
|
|
11
|
+
|
|
12
|
+
// 创建项目的主处理函数
|
|
13
|
+
const beginAction = async (project, args) => {
|
|
14
|
+
// 获取翻译函数
|
|
15
|
+
const t = i18n.getT()
|
|
16
|
+
|
|
17
|
+
// 使用 inquirer 交互式命令行提示用户选择框架
|
|
18
|
+
const answer = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'framwork',
|
|
22
|
+
choices: config.framwork,
|
|
23
|
+
message: t('prompts.selectTemplate'),
|
|
24
|
+
},
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
// 调用下载函数获取对应框架的模板
|
|
28
|
+
downloadFun(config.foramworkUrl[answer.framwork], project)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 通用的组件/Store 添加处理函数
|
|
32
|
+
async function addComponentAction(cpnName, temPath, fileType) {
|
|
33
|
+
// 获取翻译函数
|
|
34
|
+
const t = i18n.getT()
|
|
35
|
+
|
|
36
|
+
// 边界处理:将反斜杠转换为正斜杠(处理 Windows 路径格式)
|
|
37
|
+
const dest = program.opts().dest.replace(/\\/g, '/') || 'src/components'
|
|
38
|
+
|
|
39
|
+
// 边界处理:确保路径末尾带有斜杠
|
|
40
|
+
const path = /[\\/]/.test(dest.at(-1)) ? dest : `${dest}/`
|
|
41
|
+
|
|
42
|
+
// 从目标路径中提取文件夹名称(用作默认组件名)
|
|
43
|
+
const [folderName = 'index'] = dest.match(/[^\\/]+(?=\/?$)/)
|
|
44
|
+
// 如果组件名为 'index',使用文件夹名作为实际组件名
|
|
45
|
+
const name = cpnName === 'index' ? folderName : cpnName
|
|
46
|
+
|
|
47
|
+
// 使用 EJS 模板引擎编译模板文件
|
|
48
|
+
const result = await compileEjs(temPath, {
|
|
49
|
+
name: name,
|
|
50
|
+
lowername: name.toLowerCase(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// 拼接完整的文件路径
|
|
54
|
+
const filePath = `${path}${cpnName}.${fileType}`
|
|
55
|
+
|
|
56
|
+
// 将编译后的内容写入文件
|
|
57
|
+
await writeFile(filePath, result)
|
|
58
|
+
|
|
59
|
+
// 输出成功提示信息
|
|
60
|
+
console.log(chalk.green.bold(t('messages.componentCreated')), chalk.blue.bold(`${cpnName}.${fileType}`), chalk.underline(filePath))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 添加 Vue 组件
|
|
64
|
+
async function addVueComponentAction(cpnName) {
|
|
65
|
+
await addComponentAction(cpnName, 'component.vue.ejs', 'vue')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 添加 React JSX 组件
|
|
69
|
+
async function addReactComponentAction(cpnName) {
|
|
70
|
+
await addComponentAction(cpnName, 'component.jsx.ejs', 'jsx')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 添加 React TSX 组件
|
|
74
|
+
async function addReactTsComponentAction(cpnName) {
|
|
75
|
+
await addComponentAction(cpnName, 'component.tsx.ejs', 'tsx')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 添加 Redux JSX Store
|
|
79
|
+
async function addReduxStoreAction(cpnName) {
|
|
80
|
+
await addComponentAction(cpnName, 'reduxStore.jsx.ejs', 'jsx')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 添加 Redux TSX Store
|
|
84
|
+
async function addReduxTsStoreAction(cpnName) {
|
|
85
|
+
await addComponentAction(cpnName, 'reduxTsStore.tsx.ejs', 'tsx')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
beginAction,
|
|
90
|
+
addVueComponentAction,
|
|
91
|
+
addReactComponentAction,
|
|
92
|
+
addReactTsComponentAction,
|
|
93
|
+
addReduxStoreAction,
|
|
94
|
+
addReduxTsStoreAction,
|
|
95
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 关闭端口命令处理文件 - 查找并结束占用指定端口的进程
|
|
2
|
+
|
|
3
|
+
const { exec: execWithCallback } = require('child_process')
|
|
4
|
+
const { promisify } = require('util')
|
|
5
|
+
|
|
6
|
+
// 将 Node 回调风格的 exec 转换为 Promise,方便 async/await 调用
|
|
7
|
+
const defaultExec = promisify(execWithCallback)
|
|
8
|
+
|
|
9
|
+
// 端口参数标准化:校验端口必须是 1-65535 之间的数字
|
|
10
|
+
function normalizePort(port) {
|
|
11
|
+
const value = String(port || '').trim()
|
|
12
|
+
|
|
13
|
+
if (!/^\d+$/.test(value)) {
|
|
14
|
+
throw new Error('Invalid port. Port must be a number between 1 and 65535.')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const number = Number(value)
|
|
18
|
+
if (number < 1 || number > 65535) {
|
|
19
|
+
throw new Error('Invalid port. Port must be a number between 1 and 65535.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 解析命令行端口输入,支持空格分隔和逗号分隔两种方式
|
|
26
|
+
function parsePortInputs(inputs) {
|
|
27
|
+
return unique(
|
|
28
|
+
inputs
|
|
29
|
+
.flatMap((input) => String(input).split(','))
|
|
30
|
+
.map((port) => port.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map(normalizePort),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 从 netstat 的本地地址字段中提取端口号
|
|
37
|
+
function getLocalPort(localAddress) {
|
|
38
|
+
const match = String(localAddress).match(/:(\d+)$/)
|
|
39
|
+
return match ? Number(match[1]) : null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 数组去重,保持原有顺序
|
|
43
|
+
function unique(values) {
|
|
44
|
+
return Array.from(new Set(values))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 解析 Windows netstat 输出,提取占用目标端口的进程 PID
|
|
48
|
+
function parsePidsFromWindowsNetstat(output, port) {
|
|
49
|
+
return unique(
|
|
50
|
+
String(output)
|
|
51
|
+
.split(/\r?\n/)
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map((line) => line.split(/\s+/))
|
|
55
|
+
.filter((parts) => {
|
|
56
|
+
const protocol = parts[0]
|
|
57
|
+
const localAddress = parts[1]
|
|
58
|
+
const state = parts[parts.length - 2]
|
|
59
|
+
|
|
60
|
+
// 只处理本地端口匹配的连接
|
|
61
|
+
if (getLocalPort(localAddress) !== port) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// TCP 只结束监听进程,UDP 没有 LISTENING 状态,直接按端口匹配
|
|
66
|
+
return protocol === 'UDP' || (protocol === 'TCP' && state === 'LISTENING')
|
|
67
|
+
})
|
|
68
|
+
.map((parts) => parts[parts.length - 1])
|
|
69
|
+
.filter((pid) => /^\d+$/.test(pid)),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 解析类 Unix 系统 lsof 输出,提取进程 PID
|
|
74
|
+
function parsePidsFromPosixLsof(output) {
|
|
75
|
+
return unique(
|
|
76
|
+
String(output)
|
|
77
|
+
.split(/\r?\n/)
|
|
78
|
+
.map((line) => line.trim())
|
|
79
|
+
.filter((line) => /^\d+$/.test(line)),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 根据当前系统查找占用端口的进程 PID
|
|
84
|
+
async function findPortPids(port, options) {
|
|
85
|
+
const exec = options.exec || defaultExec
|
|
86
|
+
const platform = options.platform || process.platform
|
|
87
|
+
|
|
88
|
+
// Windows 使用 netstat 查询所有端口,再在 Node 内部解析过滤
|
|
89
|
+
if (platform === 'win32') {
|
|
90
|
+
const { stdout } = await exec('netstat -ano')
|
|
91
|
+
return parsePidsFromWindowsNetstat(stdout, port)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// macOS/Linux 使用 lsof 查询端口;如果系统未安装 lsof 或无占用,则视为无进程
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await exec(`lsof -ti :${port}`)
|
|
97
|
+
return parsePidsFromPosixLsof(stdout)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 根据平台结束指定 PID
|
|
104
|
+
async function killPid(pid, options) {
|
|
105
|
+
const exec = options.exec || defaultExec
|
|
106
|
+
const platform = options.platform || process.platform
|
|
107
|
+
const command = platform === 'win32' ? `taskkill /F /PID ${pid}` : `kill -9 ${pid}`
|
|
108
|
+
|
|
109
|
+
await exec(command)
|
|
110
|
+
return pid
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 关闭单个端口:查找占用进程并逐个结束
|
|
114
|
+
async function closePort(port, options = {}) {
|
|
115
|
+
const normalizedPort = normalizePort(port)
|
|
116
|
+
const pids = await findPortPids(normalizedPort, options)
|
|
117
|
+
const killed = []
|
|
118
|
+
|
|
119
|
+
for (const pid of pids) {
|
|
120
|
+
killed.push(await killPid(pid, options))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
port: normalizedPort,
|
|
125
|
+
pids,
|
|
126
|
+
killed,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 关闭多个端口:先解析输入,再按端口顺序逐个处理
|
|
131
|
+
async function closePorts(portInputs, options = {}) {
|
|
132
|
+
const ports = parsePortInputs(portInputs)
|
|
133
|
+
const results = []
|
|
134
|
+
|
|
135
|
+
for (const port of ports) {
|
|
136
|
+
results.push(await closePort(port, options))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// CLI action:执行关闭端口命令并输出结果
|
|
143
|
+
async function handleClosePortAction(ports) {
|
|
144
|
+
const chalk = require('chalk')
|
|
145
|
+
const i18n = require('../i18n')
|
|
146
|
+
const t = i18n.getT()
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const results = await closePorts(ports)
|
|
150
|
+
|
|
151
|
+
// 每个端口单独输出处理结果,方便用户定位具体端口状态
|
|
152
|
+
for (const result of results) {
|
|
153
|
+
if (result.killed.length === 0) {
|
|
154
|
+
console.log(chalk.yellow.bold(t('messages.closePortNoProcess', { port: result.port })))
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(chalk.green.bold(t('messages.closePortSuccess', { port: result.port, pids: result.killed.join(', ') })))
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.log(chalk.red.bold(err.message))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
closePort,
|
|
167
|
+
closePorts,
|
|
168
|
+
handleClosePortAction,
|
|
169
|
+
normalizePort,
|
|
170
|
+
parsePortInputs,
|
|
171
|
+
parsePidsFromPosixLsof,
|
|
172
|
+
parsePidsFromWindowsNetstat,
|
|
173
|
+
}
|