ylyx-cli 1.0.0 → 1.0.2
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/README.md +226 -184
- package/bin/ylyx.js +39 -0
- package/lib/config.js +300 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,184 +1,226 @@
|
|
|
1
|
-
# YLYX CLI - 项目脚手架工具
|
|
2
|
-
|
|
3
|
-
公司内部使用的项目脚手架工具,快速生成项目初始结构和代码模板。
|
|
4
|
-
|
|
5
|
-
## 功能特性
|
|
6
|
-
|
|
7
|
-
- 🚀 快速生成项目代码
|
|
8
|
-
- 📦 模板化管理,易于维护和扩展
|
|
9
|
-
- ⚙️ 支持自定义变量和配置
|
|
10
|
-
- ☁️ 支持云端模板(GitHub/GitLab)
|
|
11
|
-
- 📝 交互式命令行界面
|
|
12
|
-
|
|
13
|
-
## 安装
|
|
14
|
-
|
|
15
|
-
### 全局安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install -g
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### 使用 npx(推荐,无需安装)
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# 使用 npx 直接运行
|
|
25
|
-
npx
|
|
26
|
-
|
|
27
|
-
# 生成项目
|
|
28
|
-
npx
|
|
29
|
-
|
|
30
|
-
# 安装远程模板
|
|
31
|
-
npx
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### 本地开发
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
git clone <repository>
|
|
38
|
-
cd ylyx-cli
|
|
39
|
-
npm install
|
|
40
|
-
npm link # 链接到全局,可以本地测试
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 使用方法
|
|
44
|
-
|
|
45
|
-
### 快速开始
|
|
46
|
-
|
|
47
|
-
使用交互式方式创建新项目(推荐):
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
ylyx create
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
或者直接指定模板和项目名:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
# 创建一个 React 项目
|
|
57
|
-
ylyx generate react-app -n my-project -o ./my-project
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### 查看可用模板
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
ylyx list
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 安装远程模板
|
|
67
|
-
|
|
68
|
-
从 GitHub/GitLab 安装模板:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
# 使用简写格式(GitHub)
|
|
72
|
-
ylyx install owner/repo
|
|
73
|
-
|
|
74
|
-
# 使用完整 URL
|
|
75
|
-
ylyx install https://github.com/owner/repo
|
|
76
|
-
|
|
77
|
-
# 指定分支和模板名
|
|
78
|
-
ylyx install owner/repo -b develop -n my-template
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 添加本地模板
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
ylyx add template-name /path/to/template
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 查看模板详情
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
ylyx info template-name
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
1
|
+
# YLYX CLI - 项目脚手架工具
|
|
2
|
+
|
|
3
|
+
公司内部使用的项目脚手架工具,快速生成项目初始结构和代码模板。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 🚀 快速生成项目代码
|
|
8
|
+
- 📦 模板化管理,易于维护和扩展
|
|
9
|
+
- ⚙️ 支持自定义变量和配置
|
|
10
|
+
- ☁️ 支持云端模板(GitHub/GitLab)
|
|
11
|
+
- 📝 交互式命令行界面
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
### 全局安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g ylyx-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 使用 npx(推荐,无需安装)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 使用 npx 直接运行
|
|
25
|
+
npx ylyx-cli create
|
|
26
|
+
|
|
27
|
+
# 生成项目
|
|
28
|
+
npx ylyx-cli generate react-app -n my-project -o ./my-project
|
|
29
|
+
|
|
30
|
+
# 安装远程模板
|
|
31
|
+
npx ylyx-cli install owner/repo
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 本地开发
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone <repository>
|
|
38
|
+
cd ylyx-cli
|
|
39
|
+
npm install
|
|
40
|
+
npm link # 链接到全局,可以本地测试
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 使用方法
|
|
44
|
+
|
|
45
|
+
### 快速开始
|
|
46
|
+
|
|
47
|
+
使用交互式方式创建新项目(推荐):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ylyx create
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
或者直接指定模板和项目名:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 创建一个 React 项目
|
|
57
|
+
ylyx generate react-app -n my-project -o ./my-project
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 查看可用模板
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ylyx list
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 安装远程模板
|
|
67
|
+
|
|
68
|
+
从 GitHub/GitLab 安装模板:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 使用简写格式(GitHub)
|
|
72
|
+
ylyx install owner/repo
|
|
73
|
+
|
|
74
|
+
# 使用完整 URL
|
|
75
|
+
ylyx install https://github.com/owner/repo
|
|
76
|
+
|
|
77
|
+
# 指定分支和模板名
|
|
78
|
+
ylyx install owner/repo -b develop -n my-template
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 添加本地模板
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ylyx add template-name /path/to/template
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 查看模板详情
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
ylyx info template-name
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 写入配置(mode)
|
|
94
|
+
|
|
95
|
+
将当前目录的 `.ylyxrc.json` 写入/更新 `mode`(仅支持 `dev` / `prod`):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ylyx config --mode dev
|
|
99
|
+
# 或
|
|
100
|
+
ylyx config prod
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 初始化配置(init config)
|
|
104
|
+
|
|
105
|
+
初始化当前目录的配置文件与默认配置模板(会生成/补齐 `.ylyxrc.json`,并创建 `config/default-dev.js`、`config/default-prod.js` 等;默认不覆盖已有文件):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ylyx init config
|
|
109
|
+
|
|
110
|
+
# 指定初始模式
|
|
111
|
+
ylyx init config --mode prod
|
|
112
|
+
|
|
113
|
+
# 强制覆盖已存在文件
|
|
114
|
+
ylyx init config --force
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`init config` 还会尝试在当前目录的 `package.json` 中写入 npm 前置脚本(可在 `.ylyxrc.json` 的 `preScripts` 配置脚本名):
|
|
118
|
+
|
|
119
|
+
- 默认会写入 `predev` 和 `postbuild:prod`
|
|
120
|
+
- 执行 `npm run dev` 前,会自动先执行 `ylyx config dev`(dev 使用 symlink 实时同步)
|
|
121
|
+
- 执行 `npm run build:prod` 后,会自动执行 `ylyx config prod`(将 `default-prod.js` 复制到打包输出目录)
|
|
122
|
+
|
|
123
|
+
prod 输出目录规则:
|
|
124
|
+
|
|
125
|
+
- 优先使用 `.ylyxrc.json` 的 `buildDir`
|
|
126
|
+
- 否则读取当前项目 `.env.production` 的 `VUE_APP_PUBLIC_URL`,默认写入 `<VUE_APP_PUBLIC_URL>/default.js`(会去掉首尾 `/`)
|
|
127
|
+
|
|
128
|
+
## 云端模板
|
|
129
|
+
|
|
130
|
+
模板可以放在 Git 仓库中(如 GitHub、GitLab),使用 `ylyx install` 命令安装。
|
|
131
|
+
|
|
132
|
+
### 模板仓库要求
|
|
133
|
+
|
|
134
|
+
模板仓库需要包含以下结构:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
your-template/
|
|
138
|
+
template.json # 模板配置文件(可选)
|
|
139
|
+
files/ # 模板文件目录
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 示例
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# 从 GitHub 安装模板
|
|
147
|
+
ylyx install github-user/react-template
|
|
148
|
+
|
|
149
|
+
# 从私有仓库安装(需要配置 SSH 密钥)
|
|
150
|
+
ylyx install git@github.com:company/templates.git
|
|
151
|
+
|
|
152
|
+
# 指定分支
|
|
153
|
+
ylyx install owner/repo -b v2.0
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 模板配置
|
|
157
|
+
|
|
158
|
+
模板应放在 `templates/` 目录下,结构如下:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
templates/
|
|
162
|
+
template-name/
|
|
163
|
+
template.json # 模板配置文件
|
|
164
|
+
files/ # 模板文件目录
|
|
165
|
+
example.js
|
|
166
|
+
example.css
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### template.json 配置示例
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"name": "template-name",
|
|
174
|
+
"description": "模板描述",
|
|
175
|
+
"version": "1.0.0",
|
|
176
|
+
"variables": {
|
|
177
|
+
"projectName": {
|
|
178
|
+
"type": "input",
|
|
179
|
+
"message": "请输入项目名称",
|
|
180
|
+
"default": "my-project"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"processFiles": ["package.json", "README.md", "*.json"],
|
|
184
|
+
"skipFiles": ["**/*.vue", "**/*.js"]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 配置文件
|
|
189
|
+
|
|
190
|
+
可以在项目根目录创建 `.ylyxrc.json` 配置文件:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"mode": "dev",
|
|
195
|
+
"publicDir": "./public",
|
|
196
|
+
"configDir": "./config",
|
|
197
|
+
"preScripts": {
|
|
198
|
+
"dev": "dev",
|
|
199
|
+
"prod": "build:prod"
|
|
200
|
+
},
|
|
201
|
+
"templatesDir": "./templates",
|
|
202
|
+
"outputDir": "./src",
|
|
203
|
+
"defaultVariables": {
|
|
204
|
+
"author": "Your Name",
|
|
205
|
+
"company": "YLYX"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 示例
|
|
211
|
+
|
|
212
|
+
### 生成完整项目
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# 生成 React 项目
|
|
216
|
+
ylyx generate react-app -n my-react-app -o ./my-react-app
|
|
217
|
+
|
|
218
|
+
# 生成后进入目录并安装依赖
|
|
219
|
+
cd my-react-app
|
|
220
|
+
npm install
|
|
221
|
+
npm start
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
ISC
|
package/bin/ylyx.js
CHANGED
|
@@ -4,6 +4,7 @@ const { program } = require('commander');
|
|
|
4
4
|
const { generate } = require('../lib/index');
|
|
5
5
|
const { listTemplates, getTemplateInfo, addTemplate } = require('../lib/template');
|
|
6
6
|
const { interactiveCreate } = require('../lib/interactive');
|
|
7
|
+
const { setMode, initConfig } = require('../lib/config');
|
|
7
8
|
const pkg = require('../package.json');
|
|
8
9
|
|
|
9
10
|
program.name('ylyx').description('公司内部代码生成模板脚手架工具').version(pkg.version);
|
|
@@ -15,6 +16,44 @@ program
|
|
|
15
16
|
await interactiveCreate();
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
program
|
|
20
|
+
.command('config [mode]')
|
|
21
|
+
.description('写入/更新 .ylyxrc.json 配置(目前支持:mode=dev|prod)')
|
|
22
|
+
.option('-m, --mode <mode>', '运行模式:dev | prod')
|
|
23
|
+
.option('--symlink', '切换 default.js 时强制使用 symlink(dev 默认)')
|
|
24
|
+
.option('--copy', '切换 default.js 时强制使用 copy')
|
|
25
|
+
.action((modeArg, options) => {
|
|
26
|
+
try {
|
|
27
|
+
const mode = options.mode || modeArg;
|
|
28
|
+
if (!mode) {
|
|
29
|
+
console.error('❌ 缺少参数:mode(dev|prod)');
|
|
30
|
+
console.error('示例:ylyx config --mode dev 或 ylyx config dev');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const method = options.copy ? 'copy' : options.symlink ? 'symlink' : undefined;
|
|
34
|
+
setMode(mode, { method });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('❌ 配置失败:', error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('init')
|
|
43
|
+
.description('初始化相关资源')
|
|
44
|
+
.command('config')
|
|
45
|
+
.description('初始化配置(生成 .ylyxrc.json / config/default-*.js 等)')
|
|
46
|
+
.option('-m, --mode <mode>', '初始 mode:dev | prod(默认 dev)')
|
|
47
|
+
.option('-f, --force', '强制覆盖已存在文件')
|
|
48
|
+
.action((options) => {
|
|
49
|
+
try {
|
|
50
|
+
initConfig(options);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('❌ 初始化配置失败:', error.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
18
57
|
program
|
|
19
58
|
.command('generate <template-name>')
|
|
20
59
|
.alias('g')
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { log } = require('./utils');
|
|
4
|
+
|
|
5
|
+
function getConfigPath() {
|
|
6
|
+
return path.join(process.cwd(), '.ylyxrc.json');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeMode(mode) {
|
|
10
|
+
if (typeof mode !== 'string') return null;
|
|
11
|
+
const m = mode.trim().toLowerCase();
|
|
12
|
+
if (m === 'dev' || m === 'prod') return m;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseDotEnvFile(envPath) {
|
|
17
|
+
if (!fs.existsSync(envPath)) return {};
|
|
18
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
19
|
+
const lines = content.split(/\r?\n/);
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const raw of lines) {
|
|
22
|
+
const line = raw.trim();
|
|
23
|
+
if (!line || line.startsWith('#')) continue;
|
|
24
|
+
const idx = line.indexOf('=');
|
|
25
|
+
if (idx < 0) continue;
|
|
26
|
+
const key = line.slice(0, idx).trim();
|
|
27
|
+
let value = line.slice(idx + 1).trim();
|
|
28
|
+
if (
|
|
29
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
30
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
31
|
+
) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizePublicUrlToDir(publicUrl) {
|
|
40
|
+
if (!publicUrl || typeof publicUrl !== 'string') return '';
|
|
41
|
+
let v = publicUrl.trim();
|
|
42
|
+
v = v.replace(/\\/g, '/');
|
|
43
|
+
// ignore full urls
|
|
44
|
+
if (/^https?:\/\//i.test(v)) return '';
|
|
45
|
+
v = v.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
46
|
+
return v;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveProdBuildDir(config) {
|
|
50
|
+
// 1) 优先使用 .ylyxrc.json 的 buildDir(可为相对/绝对路径)
|
|
51
|
+
if (config.buildDir) {
|
|
52
|
+
return path.resolve(process.cwd(), config.buildDir);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2) 默认:读取 .env.production 的 VUE_APP_PUBLIC_URL,拼成 <publicUrlDir>
|
|
56
|
+
const envProdPath = path.join(process.cwd(), '.env.production');
|
|
57
|
+
const env = parseDotEnvFile(envProdPath);
|
|
58
|
+
const publicUrlDir = normalizePublicUrlToDir(env.VUE_APP_PUBLIC_URL);
|
|
59
|
+
if (publicUrlDir) {
|
|
60
|
+
return path.resolve(process.cwd(), publicUrlDir);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(
|
|
63
|
+
'无法确定 prod 打包输出目录:请在 .ylyxrc.json 中配置 buildDir,或在 .env.production 中配置 VUE_APP_PUBLIC_URL'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readConfigSafe(configPath) {
|
|
68
|
+
if (!fs.existsSync(configPath)) return {};
|
|
69
|
+
try {
|
|
70
|
+
return fs.readJsonSync(configPath);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
throw new Error(`读取 .ylyxrc.json 失败,请确认它是有效 JSON:${e.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeIfAllowed(filePath, content, force) {
|
|
77
|
+
if (fs.existsSync(filePath) && !force) {
|
|
78
|
+
log.warn(`已存在,跳过:${path.relative(process.cwd(), filePath)}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
fs.ensureDirSync(path.dirname(filePath));
|
|
82
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
83
|
+
log.success(`已写入:${path.relative(process.cwd(), filePath)}`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function deriveDefaultVariantContent(baseContent, mode) {
|
|
88
|
+
const isDev = mode === 'dev';
|
|
89
|
+
const isProd = mode === 'prod';
|
|
90
|
+
|
|
91
|
+
let content = baseContent || '';
|
|
92
|
+
const devRe = /let\s+isDev\s*=\s*[^;]*;/;
|
|
93
|
+
const prodRe = /let\s+isProd\s*=\s*[^;]*;/;
|
|
94
|
+
|
|
95
|
+
if (devRe.test(content)) {
|
|
96
|
+
content = content.replace(devRe, `let isDev = ${isDev};`);
|
|
97
|
+
} else {
|
|
98
|
+
content = `let isDev = ${isDev};\n` + content;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (prodRe.test(content)) {
|
|
102
|
+
content = content.replace(prodRe, `let isProd = ${isProd};`);
|
|
103
|
+
} else {
|
|
104
|
+
content = `let isProd = ${isProd};\n` + content;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return content;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureSymlinkOrCopy({ src, dest, preferSymlink }) {
|
|
111
|
+
fs.ensureDirSync(path.dirname(dest));
|
|
112
|
+
if (!fs.existsSync(src)) {
|
|
113
|
+
log.warn(`未找到源文件:${path.relative(process.cwd(), src)}`);
|
|
114
|
+
return { ok: false, method: 'missing' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 如果目标已存在,先删除(不管是文件还是链接)
|
|
118
|
+
if (fs.existsSync(dest)) {
|
|
119
|
+
fs.removeSync(dest);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (preferSymlink) {
|
|
123
|
+
try {
|
|
124
|
+
// Windows/macOS/Linux 都支持;Windows 可能因为权限失败,失败后回退 copy
|
|
125
|
+
fs.ensureSymlinkSync(src, dest, 'file');
|
|
126
|
+
return { ok: true, method: 'symlink' };
|
|
127
|
+
} catch (e) {
|
|
128
|
+
log.warn(`创建 symlink 失败,将降级为 copy:${e.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
fs.copyFileSync(src, dest);
|
|
134
|
+
return { ok: true, method: 'copy' };
|
|
135
|
+
} catch (e) {
|
|
136
|
+
log.error(`写入失败:${e.message}`);
|
|
137
|
+
return { ok: false, method: 'copy_failed' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 写入/更新 .ylyxrc.json 的 mode 字段(dev|prod),并与已有配置合并
|
|
143
|
+
*/
|
|
144
|
+
function setMode(mode, options = {}) {
|
|
145
|
+
const normalized = normalizeMode(mode);
|
|
146
|
+
if (!normalized) {
|
|
147
|
+
throw new Error(`mode 取值只能是 dev 或 prod(当前: ${mode})`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const configPath = getConfigPath();
|
|
151
|
+
const config = readConfigSafe(configPath);
|
|
152
|
+
|
|
153
|
+
// 可选:如果当前项目目录存在 public/default-{mode}.js,则复制到 public/default.js
|
|
154
|
+
// 注意:切换应发生在“项目目录”(process.cwd()),而不是 CLI 自身的 lib 目录(__dirname)
|
|
155
|
+
// 优先从 .ylyxrc.json 中读取 publicDir,其次使用默认 ./public
|
|
156
|
+
const configDir = path.resolve(process.cwd(), config.configDir || 'config');
|
|
157
|
+
const publicDir = path.resolve(process.cwd(), config.publicDir || 'public');
|
|
158
|
+
const defaultVariant = path.join(configDir, `default-${normalized}.js`);
|
|
159
|
+
const defaultFile =
|
|
160
|
+
normalized === 'prod'
|
|
161
|
+
? path.join(resolveProdBuildDir(config), 'default.js')
|
|
162
|
+
: path.join(publicDir, 'default.js');
|
|
163
|
+
|
|
164
|
+
// 默认策略:dev 使用 symlink(实时同步),prod 使用 copy(稳定)
|
|
165
|
+
const method = options.method || (normalized === 'dev' ? 'symlink' : 'copy');
|
|
166
|
+
const result = ensureSymlinkOrCopy({
|
|
167
|
+
src: defaultVariant,
|
|
168
|
+
dest: defaultFile,
|
|
169
|
+
preferSymlink: method === 'symlink',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
const label = result.method === 'symlink' ? 'symlink' : 'copy';
|
|
174
|
+
log.success(
|
|
175
|
+
`已(${label})切换 ${path.relative(process.cwd(), defaultFile)} <- ${path.relative(
|
|
176
|
+
process.cwd(),
|
|
177
|
+
defaultVariant
|
|
178
|
+
)}`
|
|
179
|
+
);
|
|
180
|
+
if (normalized === 'dev' && result.method === 'symlink') {
|
|
181
|
+
log.info('dev 模式实时同步已启用:修改 default-dev.js 会立即反映到 default.js');
|
|
182
|
+
}
|
|
183
|
+
} else if (result.method === 'missing') {
|
|
184
|
+
log.warn(`未找到 ${path.relative(process.cwd(), defaultVariant)},请先运行 ylyx init config`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
config.mode = normalized;
|
|
188
|
+
fs.writeJsonSync(configPath, config, { spaces: 2 });
|
|
189
|
+
log.success(`已写入配置:mode=${normalized}`);
|
|
190
|
+
log.info(`配置文件:${configPath}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* init config:初始化配置目录与示例文件
|
|
195
|
+
* - 写入/补齐 .ylyxrc.json(mode/publicDir/configDir)
|
|
196
|
+
* - 生成 config/default-dev.js 与 config/default-prod.js(基于现有 public/default.js 派生)
|
|
197
|
+
* - 若 public/default.js 不存在,则按当前 mode 生成一份
|
|
198
|
+
*/
|
|
199
|
+
function initConfig(options = {}) {
|
|
200
|
+
const force = !!options.force;
|
|
201
|
+
const configPath = getConfigPath();
|
|
202
|
+
const existing = readConfigSafe(configPath);
|
|
203
|
+
|
|
204
|
+
const next = {
|
|
205
|
+
...existing,
|
|
206
|
+
mode: existing.mode || 'dev',
|
|
207
|
+
publicDir: existing.publicDir || './public',
|
|
208
|
+
configDir: existing.configDir || './config',
|
|
209
|
+
// npm scripts:默认对 dev 与 build:prod 增加前置脚本 pre<name>
|
|
210
|
+
// 可在 .ylyxrc.json 中改成你的项目脚本名(例如 dev=serve,prod=build)
|
|
211
|
+
preScripts: existing.preScripts || { dev: 'dev', prod: 'build:prod' },
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (options.mode) {
|
|
215
|
+
const normalized = normalizeMode(options.mode);
|
|
216
|
+
if (!normalized) throw new Error(`mode 取值只能是 dev 或 prod(当前: ${options.mode})`);
|
|
217
|
+
next.mode = normalized;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.writeJsonSync(configPath, next, { spaces: 2 });
|
|
221
|
+
log.success(`已初始化配置文件:${path.relative(process.cwd(), configPath)}`);
|
|
222
|
+
|
|
223
|
+
const publicDirAbs = path.resolve(process.cwd(), next.publicDir || 'public');
|
|
224
|
+
const configDirAbs = path.resolve(process.cwd(), next.configDir || 'config');
|
|
225
|
+
fs.ensureDirSync(publicDirAbs);
|
|
226
|
+
fs.ensureDirSync(configDirAbs);
|
|
227
|
+
|
|
228
|
+
const publicDefault = path.join(publicDirAbs, 'default.js');
|
|
229
|
+
let baseContent = '';
|
|
230
|
+
if (fs.existsSync(publicDefault)) {
|
|
231
|
+
baseContent = fs.readFileSync(publicDefault, 'utf-8');
|
|
232
|
+
} else {
|
|
233
|
+
baseContent =
|
|
234
|
+
"// 自动生成的默认配置(请按项目需要补全)\n" +
|
|
235
|
+
"let isDev = true;\n"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const devVariantPath = path.join(configDirAbs, 'default-dev.js');
|
|
239
|
+
const prodVariantPath = path.join(configDirAbs, 'default-prod.js');
|
|
240
|
+
|
|
241
|
+
writeIfAllowed(devVariantPath, deriveDefaultVariantContent(baseContent, 'dev'), force);
|
|
242
|
+
writeIfAllowed(prodVariantPath, deriveDefaultVariantContent(baseContent, 'prod'), force);
|
|
243
|
+
|
|
244
|
+
// 如果 public/default.js 不存在,则按当前 mode 生成一份
|
|
245
|
+
if (!fs.existsSync(publicDefault)) {
|
|
246
|
+
const src =
|
|
247
|
+
next.mode === 'prod'
|
|
248
|
+
? path.join(configDirAbs, 'default-prod.js')
|
|
249
|
+
: path.join(configDirAbs, 'default-dev.js');
|
|
250
|
+
if (fs.existsSync(src)) {
|
|
251
|
+
fs.copyFileSync(src, publicDefault);
|
|
252
|
+
log.success(
|
|
253
|
+
`已生成 ${path.relative(process.cwd(), publicDefault)} <- ${path.relative(process.cwd(), src)}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
log.info(`已存在:${path.relative(process.cwd(), publicDefault)}(未覆盖)`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 初始化 package.json 的 npm 前置脚本:pre<devScript> / pre<prodScript>
|
|
261
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
262
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
263
|
+
log.warn('未找到 package.json,已跳过 npm 前置脚本初始化');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let pkg = {};
|
|
268
|
+
try {
|
|
269
|
+
pkg = fs.readJsonSync(packageJsonPath);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
throw new Error(`读取 package.json 失败,请确认它是有效 JSON:${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
pkg.scripts = pkg.scripts || {};
|
|
275
|
+
const devTarget = (next.preScripts && next.preScripts.dev) || 'dev';
|
|
276
|
+
const prodTarget = (next.preScripts && next.preScripts.prod) || 'build:prod';
|
|
277
|
+
|
|
278
|
+
const desired = [
|
|
279
|
+
{ name: `pre${devTarget}`, value: 'ylyx config dev' },
|
|
280
|
+
{ name: `post${prodTarget}`, value: 'ylyx config prod' },
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const item of desired) {
|
|
284
|
+
if (pkg.scripts[item.name] && !force) {
|
|
285
|
+
log.warn(`package.json scripts 已存在,跳过:${item.name}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
pkg.scripts[item.name] = item.value;
|
|
289
|
+
log.success(`已写入 package.json scripts:${item.name}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fs.writeJsonSync(packageJsonPath, pkg, { spaces: 2 });
|
|
293
|
+
log.info(`package.json:${path.relative(process.cwd(), packageJsonPath)}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
setMode,
|
|
298
|
+
normalizeMode,
|
|
299
|
+
initConfig,
|
|
300
|
+
};
|