juice-email-cli 2.1.6 → 2.1.8
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 +162 -112
- package/bin/juice.js +38 -22
- package/defaults/juice.yaml +4 -0
- package/package.json +1 -1
- package/src/context-menu.js +6 -8
- package/src/index.js +6 -1
- package/src/snippet.js +176 -15
package/README.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# juice-email-cli
|
|
2
2
|
|
|
3
|
-
> 一个用于生成符合各大邮件平台标准的 HTML 邮件命令行工具,基于 [juice](https://github.com/Automattic/juice) 实现 CSS 内联,支持 Mustache
|
|
3
|
+
> 一个用于生成符合各大邮件平台标准的 HTML 邮件命令行工具,基于 [juice](https://github.com/Automattic/juice) 实现 CSS 内联,支持 Mustache 模板变量替换,同时输出标准版与压缩版。
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
9
|
+
- **CSS 内联** —— 将 `<style>` 中的样式全部内联为 `style=""` 属性,兼容 Gmail / Outlook / Apple Mail 等
|
|
10
|
+
- **Mustache 模板变量** —— 支持 `{{变量名}}` 语法,通过配置文件批量替换
|
|
11
|
+
- **Mustache 列表循环** —— 支持 `{{#items}}...{{/items}}` 遍历数组,支持嵌套循环
|
|
12
|
+
- **片段组装** —— 将片段 HTML 插入模板的 `<tbody id="content">`,自动调整缩进,输出 4 个阶段文件
|
|
13
|
+
- **三层配置合并** —— CLI 默认 < 用户目录 < 优先配置,层层合并
|
|
14
|
+
- **HTML 标签渲染** —— 变量值中的 `<sup>`、`<sub>` 等 HTML 标签直接渲染(可通过 `rawHtml` 关闭)
|
|
15
|
+
- **双文件输出** —— 普通模式生成 `.output.html` + `.minified.html`
|
|
16
|
+
- **四文件输出** —— 片段模式生成 `.raw.html` + `.html` + `.output.html` + `.minified.html`
|
|
17
|
+
- **交互模式** —— 无参数运行,逐步选择品牌、模板、片段、配置
|
|
18
|
+
- **Windows 右键菜单** —— 子菜单层级,一键注册/卸载
|
|
16
19
|
|
|
17
20
|
---
|
|
18
21
|
|
|
@@ -42,32 +45,79 @@ npm link # 链接到全局
|
|
|
42
45
|
|
|
43
46
|
## 使用方法
|
|
44
47
|
|
|
48
|
+
### 普通模式(CSS 内联 + 压缩,输出 2 个文件)
|
|
49
|
+
|
|
45
50
|
```bash
|
|
46
|
-
#
|
|
51
|
+
# 最简用法
|
|
47
52
|
juice -f my-email.html
|
|
48
53
|
|
|
49
54
|
# 指定配置文件
|
|
50
55
|
juice -c project.yaml -f my-email.html
|
|
56
|
+
|
|
57
|
+
# 使用 EDM 模板库中的模板
|
|
58
|
+
juice -f edm/elabscience/elabscience-template.html
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
生成文件(与输入文件同目录):
|
|
62
|
+
|
|
63
|
+
| 文件 | 说明 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| `<name>.output.html` | CSS 内联 + 变量替换后的标准版 |
|
|
66
|
+
| `<name>.minified.html` | 压缩版 |
|
|
67
|
+
|
|
68
|
+
### 片段模式(片段 + 模板拼接,输出 4 个文件)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 完整指定片段和模板
|
|
72
|
+
juice -s edm/elabscience/literature/snippet.html -f edm/elabscience/elabscience-template.html
|
|
73
|
+
|
|
74
|
+
# 只指定片段,交互式选择模板
|
|
75
|
+
juice -s edm/elabscience/literature/snippet.html
|
|
76
|
+
|
|
77
|
+
# 自定义输出文件名
|
|
78
|
+
juice -s snippet.html -f template.html -n my-output
|
|
51
79
|
```
|
|
52
80
|
|
|
53
|
-
|
|
81
|
+
生成文件(当前工作目录):
|
|
54
82
|
|
|
55
83
|
| 文件 | 说明 |
|
|
56
84
|
|------|------|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
85
|
+
| `<name>.raw.html` | 原始组装(Mustache 未渲染,无 CSS 内联) |
|
|
86
|
+
| `<name>.html` | 已渲染(Mustache 变量已替换,无 CSS 内联) |
|
|
87
|
+
| `<name>.output.html` | Juice CSS 内联后 |
|
|
88
|
+
| `<name>.minified.html` | 压缩版 |
|
|
89
|
+
|
|
90
|
+
输出文件名默认为模板文件名(不含扩展名)。如果文件冲突,交互模式下可选择覆盖、自动版本号(`-v1`、`-v2`...)或重新输入。
|
|
91
|
+
|
|
92
|
+
### 交互模式(逐步选择)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
juice
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
流程:选择品牌 → 模板 → 片段文件夹 → 片段 HTML → 配置文件 → 输出文件名 → 确认执行。
|
|
59
99
|
|
|
60
100
|
### 参数说明
|
|
61
101
|
|
|
62
102
|
| 参数 | 简写 | 说明 |
|
|
63
103
|
|------|------|------|
|
|
64
|
-
| `--file <path>` | `-f` | 输入 HTML
|
|
65
|
-
| `--
|
|
104
|
+
| `--file <path>` | `-f` | 输入 HTML 模板文件路径 |
|
|
105
|
+
| `--snippet <path>` | `-s` | 片段 HTML 文件路径:插入到模板 `<tbody id="content">` |
|
|
106
|
+
| `--config <path>` | `-c` | 配置文件路径,不指定时自动查找 |
|
|
107
|
+
| `--name <name>` | `-n` | 片段模式输出文件名(不含扩展名) |
|
|
66
108
|
| `--install` | | 注册 Windows 右键菜单(需管理员权限) |
|
|
67
109
|
| `--uninstall` | | 取消 Windows 右键菜单注册(需管理员权限) |
|
|
68
110
|
| `--version` | `-v` | 查看版本号 |
|
|
69
111
|
| `--help` | `-h` | 查看帮助 |
|
|
70
112
|
|
|
113
|
+
### CLI 执行模式
|
|
114
|
+
|
|
115
|
+
| `-s` | `-f` | `-c` | 执行模式 |
|
|
116
|
+
|------|------|------|----------|
|
|
117
|
+
| ✓ | * | * | 片段模式(-s 指定片段,-f 可选指定模板) |
|
|
118
|
+
| ✗ | ✓ | * | 普通模式(生成 .output.html + .minified.html) |
|
|
119
|
+
| ✗ | ✗ | * | 交互式片段模式(逐步选择) |
|
|
120
|
+
|
|
71
121
|
---
|
|
72
122
|
|
|
73
123
|
## 配置文件优先级
|
|
@@ -97,11 +147,17 @@ juice -c project.yaml -f my-email.html
|
|
|
97
147
|
juice -c project.yaml -f email.html
|
|
98
148
|
|
|
99
149
|
# 使用输入文件同级目录配置(与用户目录合并)
|
|
100
|
-
# → email.html 同目录下有 juice.yaml
|
|
101
150
|
juice -f email.html
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 片段模式配置
|
|
154
|
+
|
|
155
|
+
片段模式下,会自动检测片段目录下的 `juice.yaml` / `juice.yml` 作为项目配置参与合并:
|
|
102
156
|
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
```
|
|
158
|
+
优先级 低 ──────────────────────────────────────────────────────── 高
|
|
159
|
+
|
|
160
|
+
内置默认 < ~/juice.yaml < 片段目录 juice.yaml < -c 指定
|
|
105
161
|
```
|
|
106
162
|
|
|
107
163
|
---
|
|
@@ -111,6 +167,9 @@ juice -f email.html
|
|
|
111
167
|
用户配置文件(`~/juice.yaml` 或项目目录 `juice.yaml`)只需填写需要覆盖的字段,其余自动继承:
|
|
112
168
|
|
|
113
169
|
```yaml
|
|
170
|
+
# 设为 false 时,变量值中的 HTML 标签会被转义
|
|
171
|
+
rawHtml: true
|
|
172
|
+
|
|
114
173
|
variables:
|
|
115
174
|
brandName: "我的品牌"
|
|
116
175
|
brandColor: "#ff6600"
|
|
@@ -127,28 +186,17 @@ variables:
|
|
|
127
186
|
tag: "热销"
|
|
128
187
|
- name: "产品 B"
|
|
129
188
|
price: "¥199.00"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
items:
|
|
136
|
-
- "优势 A"
|
|
137
|
-
- "优势 B"
|
|
138
|
-
- title: "特性二"
|
|
139
|
-
items:
|
|
140
|
-
- "优势 C"
|
|
141
|
-
- "优势 D"
|
|
189
|
+
|
|
190
|
+
# 变量值支持 HTML 标签(rawHtml: true 时)
|
|
191
|
+
overview:
|
|
192
|
+
title: "CD38-NAD<sup>+</sup> Axis Study"
|
|
193
|
+
keywords: "Immune Thrombocytopenia, NMN, NAD<sup>+</sup>"
|
|
142
194
|
|
|
143
195
|
# juice 选项(全部可选,均有内置默认值)
|
|
144
196
|
# juice:
|
|
145
197
|
# removeStyleTags: true
|
|
146
198
|
# preserveMediaQueries: true
|
|
147
|
-
# preservePseudos: true
|
|
148
|
-
# preservedSelectors: # 保留的选择器(不会被内联)
|
|
149
|
-
# - "a:hover"
|
|
150
|
-
# extraCssFiles:
|
|
151
|
-
# - email-reset.css
|
|
199
|
+
# preservePseudos: true
|
|
152
200
|
|
|
153
201
|
# 输出后缀(可选)
|
|
154
202
|
# output:
|
|
@@ -169,12 +217,26 @@ variables:
|
|
|
169
217
|
<a href="{{ctaUrl}}" style="background-color: {{brandColor}};">{{ctaText}}</a>
|
|
170
218
|
```
|
|
171
219
|
|
|
220
|
+
### HTML 标签在变量中
|
|
221
|
+
|
|
222
|
+
当 `rawHtml: true`(默认)时,变量值中的 HTML 标签直接渲染:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
variables:
|
|
226
|
+
overview:
|
|
227
|
+
keywords: "CD38-NAD<sup>+</sup> Axis, NO<sub>3</sub><sup>-</sup>"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<p>{{overview.keywords}}</p>
|
|
232
|
+
<!-- 渲染为:CD38-NAD<sup>+</sup> Axis, NO<sub>3</sub><sup>-</sup> -->
|
|
233
|
+
```
|
|
234
|
+
|
|
172
235
|
### Mustache 列表循环
|
|
173
236
|
|
|
174
237
|
支持 `{{#items}}...{{/items}}` 语法遍历数组数据:
|
|
175
238
|
|
|
176
239
|
```yaml
|
|
177
|
-
# juice.yaml - 配置列表数据
|
|
178
240
|
variables:
|
|
179
241
|
products:
|
|
180
242
|
- name: "产品 A"
|
|
@@ -182,12 +244,9 @@ variables:
|
|
|
182
244
|
tag: "热销"
|
|
183
245
|
- name: "产品 B"
|
|
184
246
|
price: "¥199.00"
|
|
185
|
-
- name: "产品 C"
|
|
186
|
-
price: "¥299.00"
|
|
187
247
|
```
|
|
188
248
|
|
|
189
249
|
```html
|
|
190
|
-
<!-- HTML 模板 -->
|
|
191
250
|
{{#products}}
|
|
192
251
|
<div class="product-item">
|
|
193
252
|
<h3>{{name}}</h3>
|
|
@@ -196,55 +255,79 @@ variables:
|
|
|
196
255
|
</div>
|
|
197
256
|
{{/products}}
|
|
198
257
|
|
|
199
|
-
<!-- 空列表备选内容 -->
|
|
200
258
|
{{^products}}
|
|
201
259
|
<p>暂无商品</p>
|
|
202
260
|
{{/products}}
|
|
203
|
-
|
|
204
|
-
<!-- 嵌套循环示例 -->
|
|
205
|
-
{{#features}}
|
|
206
|
-
<div class="feature-column">
|
|
207
|
-
<h4>{{title}}</h4>
|
|
208
|
-
<ul>
|
|
209
|
-
{{#items}}
|
|
210
|
-
<li>{{.}}</li>
|
|
211
|
-
{{/items}}
|
|
212
|
-
</ul>
|
|
213
|
-
</div>
|
|
214
|
-
{{/features}}
|
|
215
261
|
```
|
|
216
262
|
|
|
217
263
|
| 语法 | 说明 | 示例 |
|
|
218
264
|
|------|------|------|
|
|
219
265
|
| `{{#list}}{{/list}}` | 循环遍历 | `{{#products}}{{name}}{{/products}}` |
|
|
220
266
|
| `{{^list}}{{/list}}` | 反向(空列表时显示) | `{{^products}}暂无{{/products}}` |
|
|
221
|
-
| `{{.}}` | 当前元素 | `{{
|
|
267
|
+
| `{{.}}` | 当前元素 | `{{#items}}{{.}}{{/items}}` |
|
|
222
268
|
| `{{#var}}{{/var}}` | 条件渲染(仅当有值时显示) | `{{#tag}}{{tag}}{{/tag}}` |
|
|
223
269
|
|
|
224
270
|
---
|
|
225
271
|
|
|
226
|
-
##
|
|
272
|
+
## 片段组装
|
|
227
273
|
|
|
228
|
-
|
|
274
|
+
### EDM 目录结构
|
|
229
275
|
|
|
230
276
|
```
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
277
|
+
edm/
|
|
278
|
+
├── <brand>/ # 品牌目录
|
|
279
|
+
│ ├── <brand>-template.html # 品牌模板(含 <tbody id="content">)
|
|
280
|
+
│ └── <series>/ # 片段系列目录
|
|
281
|
+
│ ├── snippet.html # 片段 HTML(Mustache 模板片段)
|
|
282
|
+
│ └── juice.yaml # 片段配置(variables)
|
|
234
283
|
```
|
|
235
284
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
285
|
+
### 片段 HTML 格式
|
|
286
|
+
|
|
287
|
+
片段是模板中 `<tbody id="content">` 内的内容片段,会被自动插入并调整缩进:
|
|
239
288
|
|
|
240
|
-
|
|
241
|
-
|
|
289
|
+
```html
|
|
290
|
+
<!-- edm/elabscience/literature/snippet.html -->
|
|
291
|
+
<tr>
|
|
292
|
+
<td></td>
|
|
293
|
+
<td colspan="2">
|
|
294
|
+
<table>
|
|
295
|
+
<tbody>
|
|
296
|
+
<tr>
|
|
297
|
+
<td><img src="{{overview.image}}" /></td>
|
|
298
|
+
<td><strong>{{overview.title}}</strong></td>
|
|
299
|
+
</tr>
|
|
300
|
+
</tbody>
|
|
301
|
+
</table>
|
|
302
|
+
</td>
|
|
303
|
+
<td></td>
|
|
304
|
+
</tr>
|
|
242
305
|
```
|
|
243
306
|
|
|
244
|
-
|
|
245
|
-
> 注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
307
|
+
### 跨品牌检查
|
|
246
308
|
|
|
247
|
-
|
|
309
|
+
片段和模板来自不同品牌时,会输出警告但仍继续执行,避免样式错乱。
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Windows 右键菜单
|
|
314
|
+
|
|
315
|
+
注册后,在 `.html` / `.htm` 文件上右键可看到级联子菜单:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
📧 用 juice 生成邮件 HTML
|
|
319
|
+
├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f
|
|
320
|
+
├── 🧩 邮件片段组装(交互选择模板) → juice -s
|
|
321
|
+
└── 📂 在此目录打开 PowerShell 7 ← 仅在已安装 pwsh 时出现
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# 以管理员身份运行
|
|
326
|
+
juice --install # 注册
|
|
327
|
+
juice --uninstall # 卸载
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
> **注意**:注册/卸载需要**管理员权限**。注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
248
331
|
|
|
249
332
|
---
|
|
250
333
|
|
|
@@ -256,26 +339,24 @@ juice-cli/
|
|
|
256
339
|
│ └── juice.js # CLI 入口
|
|
257
340
|
├── src/
|
|
258
341
|
│ ├── index.js # 核心逻辑(配置合并、模板处理、双输出)
|
|
259
|
-
│
|
|
342
|
+
│ ├── snippet.js # 片段组装模式 + 交互式提示
|
|
343
|
+
│ └── context-menu.js # Windows 右键菜单注册
|
|
260
344
|
├── defaults/
|
|
261
|
-
│ └── juice.yaml # CLI
|
|
262
|
-
├──
|
|
263
|
-
│ ├──
|
|
264
|
-
│ ├── template.html
|
|
265
|
-
│ └──
|
|
345
|
+
│ └── juice.yaml # CLI 内置默认配置
|
|
346
|
+
├── edm/ # EDM 模板库(npm 发布时包含)
|
|
347
|
+
│ ├── elabscience/
|
|
348
|
+
│ │ ├── elabscience-template.html
|
|
349
|
+
│ │ └── literature/
|
|
350
|
+
│ │ ├── snippet.html
|
|
351
|
+
│ │ └── juice.yaml
|
|
352
|
+
│ └── procell/
|
|
353
|
+
│ └── procell-template.html
|
|
266
354
|
├── icons/
|
|
267
|
-
│ └── juice-icon.ico #
|
|
355
|
+
│ └── juice-icon.ico # 右键菜单图标
|
|
268
356
|
├── scripts/
|
|
269
|
-
│ ├── generate-icon.js # 图标生成脚本
|
|
270
357
|
│ └── release.mjs # 发布脚本
|
|
271
|
-
├── .husky/
|
|
272
|
-
│ └── commit-msg # Git hooks - 提交信息校验
|
|
273
|
-
├── .commitlintrc.json # Commitlint 配置
|
|
274
|
-
├── .versionrc # Standard-version 配置
|
|
275
|
-
├── .npmrc # npm 发布配置
|
|
276
358
|
├── CHANGELOG.md # 变更日志(自动生成)
|
|
277
359
|
├── LICENSE # MIT
|
|
278
|
-
├── README.md
|
|
279
360
|
└── package.json
|
|
280
361
|
```
|
|
281
362
|
|
|
@@ -283,55 +364,24 @@ juice-cli/
|
|
|
283
364
|
|
|
284
365
|
## 发布说明
|
|
285
366
|
|
|
286
|
-
本项目使用自动化发布流程,包含以下功能:
|
|
287
|
-
|
|
288
367
|
### 提交信息规范
|
|
289
368
|
|
|
290
369
|
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
|
|
291
370
|
|
|
292
371
|
```
|
|
293
|
-
<type>(<scope>): <subject>
|
|
294
|
-
|
|
295
372
|
feat: 添加新功能
|
|
296
373
|
fix: 修复 bug
|
|
297
374
|
docs: 更新文档
|
|
298
|
-
style: 代码格式调整
|
|
299
|
-
refactor: 重构代码
|
|
300
|
-
perf: 性能优化
|
|
301
|
-
test: 添加测试
|
|
302
|
-
build: 构建系统变更
|
|
303
|
-
ci: CI/CD 配置变更
|
|
304
375
|
chore: 其他变更
|
|
305
|
-
revert: 回退提交
|
|
306
376
|
```
|
|
307
377
|
|
|
308
378
|
### 自动化发布
|
|
309
379
|
|
|
310
380
|
```bash
|
|
311
|
-
#
|
|
312
|
-
npm run release
|
|
313
|
-
|
|
314
|
-
# 预览发布(不实际执行)
|
|
315
|
-
npm run release:dry
|
|
381
|
+
npm run release # 一键发布
|
|
382
|
+
npm run release:dry # 预览
|
|
316
383
|
```
|
|
317
384
|
|
|
318
|
-
**发布流程:**
|
|
319
|
-
1. 检测未提交的文件,询问是否提交
|
|
320
|
-
2. 选择版本更新类型(Major / Minor / Patch)
|
|
321
|
-
3. 确认版本号
|
|
322
|
-
4. 自动更新版本号、生成 CHANGELOG、打 tag
|
|
323
|
-
5. 推送到 origin 和 github 远程仓库
|
|
324
|
-
6. GitHub Actions 自动发布到 npm
|
|
325
|
-
|
|
326
|
-
### 配置文件
|
|
327
|
-
|
|
328
|
-
| 文件 | 说明 |
|
|
329
|
-
|------|------|
|
|
330
|
-
| `.commitlintrc.json` | 提交信息校验规则 |
|
|
331
|
-
| `.versionrc` | 版本更新和 CHANGELOG 生成规则 |
|
|
332
|
-
| `.npmrc` | npm 发布配置 |
|
|
333
|
-
| `.husky/commit-msg` | Git hooks 配置 |
|
|
334
|
-
|
|
335
385
|
---
|
|
336
386
|
|
|
337
387
|
## License
|
package/bin/juice.js
CHANGED
|
@@ -11,8 +11,9 @@ program
|
|
|
11
11
|
.description('生成符合各大邮件平台的内联 CSS HTML 邮件(同时输出标准版 + 压缩版)')
|
|
12
12
|
.version(pkg.version, '-v, --version')
|
|
13
13
|
.option('-f, --file <path>', '输入 HTML 模板文件路径')
|
|
14
|
-
.option('-
|
|
15
|
-
.option('--
|
|
14
|
+
.option('-s, --snippet <path>', '片段 HTML 文件路径:将片段内容插入模板 <tbody id="content"> 中')
|
|
15
|
+
.option('-c, --config <path>', '配置文件路径')
|
|
16
|
+
.option('-n, --name <name>', '片段模式输出文件名(不含扩展名)')
|
|
16
17
|
.option('--install', '注册 Windows 右键菜单(需管理员权限)')
|
|
17
18
|
.option('--uninstall', '取消 Windows 右键菜单注册(需管理员权限)')
|
|
18
19
|
.addHelpText('before', `
|
|
@@ -27,30 +28,44 @@ program
|
|
|
27
28
|
2. 用户主目录 ~/juice.yaml(如果存在)
|
|
28
29
|
3. 优先配置(-c 指定 或 输入文件同级目录)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
════════════════════════════════════════════════════════════════
|
|
32
|
+
使用示例
|
|
33
|
+
════════════════════════════════════════════════════════════════
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
普通模式(CSS 内联 + 压缩,输出 2 个文件):
|
|
36
|
+
juice -f my-email.html
|
|
37
|
+
juice -f my-email.html -c project.yaml
|
|
38
|
+
juice -f edm/elabscience/elabscience-template.html
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
片段模式(片段 + 模板拼接,输出 4 个文件):
|
|
41
|
+
juice -s edm/elabscience/literature/snippet.html -f edm/elabscience/elabscience-template.html
|
|
42
|
+
juice -s edm/elabscience/literature/snippet.html (交互选择模板)
|
|
43
|
+
juice -s snippet.html -f template.html -n my-output (自定义输出名)
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
juice --uninstall (管理员权限,卸载右键菜单)
|
|
45
|
+
交互模式(逐步选择品牌、模板、片段、配置):
|
|
46
|
+
juice
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
════════════════════════════════════════════════════════════════
|
|
49
|
+
输出文件说明
|
|
50
|
+
════════════════════════════════════════════════════════════════
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
普通模式(与输入文件同目录):
|
|
53
|
+
<name>.output.html CSS 内联 + 变量替换后的标准版
|
|
54
|
+
<name>.minified.html 压缩版
|
|
55
|
+
|
|
56
|
+
片段模式(当前工作目录):
|
|
57
|
+
<name>.raw.html 原始组装(Mustache 未渲染,无 CSS 内联)
|
|
58
|
+
<name>.html 已渲染(Mustache 变量已替换,无 CSS 内联)
|
|
59
|
+
<name>.output.html Juice CSS 内联后
|
|
60
|
+
<name>.minified.html 压缩版
|
|
61
|
+
|
|
62
|
+
════════════════════════════════════════════════════════════════
|
|
63
|
+
右键菜单
|
|
64
|
+
════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
在 .html/.htm 文件上右键即可使用:
|
|
67
|
+
juice --install (管理员权限,注册右键菜单)
|
|
68
|
+
juice --uninstall (管理员权限,卸载右键菜单)
|
|
54
69
|
|
|
55
70
|
更多信息:https://gitee.com/siriussupreme/juice-cli
|
|
56
71
|
`)
|
|
@@ -67,13 +82,14 @@ program
|
|
|
67
82
|
return;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
// --snippet 模式:片段组装
|
|
85
|
+
// --snippet / -s 模式:片段组装
|
|
71
86
|
if (options.snippet) {
|
|
72
87
|
const { runSnippetMode } = require('../src/snippet');
|
|
73
88
|
await runSnippetMode({
|
|
74
89
|
snippet: options.snippet,
|
|
75
90
|
template: options.file,
|
|
76
91
|
config: options.config,
|
|
92
|
+
outputName: options.name,
|
|
77
93
|
});
|
|
78
94
|
return;
|
|
79
95
|
}
|
package/defaults/juice.yaml
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
#
|
|
10
10
|
# 用户配置文件(~/juice.yaml 或项目 juice.yaml)只需填写需要覆盖的字段即可。
|
|
11
11
|
|
|
12
|
+
# 设为 false 时,模板变量中的 HTML 标签会被转义(<sup> 变成 <sup>)
|
|
13
|
+
# 默认 true:变量值中的 HTML 标签(如 <sup>、<sub>)直接渲染
|
|
14
|
+
rawHtml: true
|
|
15
|
+
|
|
12
16
|
# ─── juice CSS 内联选项 ───────────────────────────────────────────────────────
|
|
13
17
|
juice:
|
|
14
18
|
# 处理完成后移除 <style> 标签(推荐 true,提高 Gmail/Outlook 等邮件客户端兼容性)
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -102,14 +102,14 @@ async function registerContextMenu() {
|
|
|
102
102
|
|
|
103
103
|
// ── 注册子命令到 CommandStore ─────────────────────────────────────────────
|
|
104
104
|
|
|
105
|
-
// 子命令 1
|
|
105
|
+
// 子命令 1:普通模式 - juice 生成
|
|
106
106
|
const generateCmd = `"${nodePath}" "${scriptPath}" -f "%1"`;
|
|
107
107
|
regAdd(`${SUBCMD_SPACE}\\${subCmd1}`, '', 'REG_SZ', '⚡ 生成邮件 HTML(标准 + 压缩)');
|
|
108
108
|
regAdd(`${SUBCMD_SPACE}\\${subCmd1}`, 'Icon', 'REG_SZ', iconPath);
|
|
109
109
|
regAdd(`${SUBCMD_SPACE}\\${subCmd1}\\command`, '', 'REG_SZ', generateCmd);
|
|
110
110
|
|
|
111
|
-
// 子命令 2
|
|
112
|
-
const snippetCmd = `"${nodePath}" "${scriptPath}"
|
|
111
|
+
// 子命令 2:片段模式 - 片段组装(交互选择模板)
|
|
112
|
+
const snippetCmd = `"${nodePath}" "${scriptPath}" -s "%1"`;
|
|
113
113
|
regAdd(`${SUBCMD_SPACE}\\${subCmd2}`, '', 'REG_SZ', '🧩 邮件片段组装(交互选择模板)');
|
|
114
114
|
regAdd(`${SUBCMD_SPACE}\\${subCmd2}`, 'Icon', 'REG_SZ', iconPath);
|
|
115
115
|
regAdd(`${SUBCMD_SPACE}\\${subCmd2}\\command`, '', 'REG_SZ', snippetCmd);
|
|
@@ -146,13 +146,11 @@ async function registerContextMenu() {
|
|
|
146
146
|
chalk.green(' ✔ 右键菜单注册完成!') + '\n\n' +
|
|
147
147
|
` 在 ${chalk.cyan('.html')} / ${chalk.cyan('.htm')} 文件上右键即可看到:\n` +
|
|
148
148
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
149
|
-
` ├── ⚡ 生成邮件 HTML(标准 +
|
|
150
|
-
` ├── 🧩
|
|
149
|
+
` ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f\n` +
|
|
150
|
+
` ├── 🧩 邮件片段组装(交互选择模板) → juice -s\n` +
|
|
151
151
|
(pwshPath ? ` └── 📂 在此目录打开 PowerShell 7\n` : '') +
|
|
152
152
|
'\n' +
|
|
153
|
-
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
154
|
-
chalk.gray(' 安装时自动注册失败?请以管理员身份重新运行:\n') +
|
|
155
|
-
chalk.cyan(' juice --register\n')
|
|
153
|
+
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
156
154
|
);
|
|
157
155
|
}
|
|
158
156
|
|
package/src/index.js
CHANGED
|
@@ -146,8 +146,13 @@ function buildConfig(highPriorityPath, homePath) {
|
|
|
146
146
|
function processTemplate(inputFile, config) {
|
|
147
147
|
const htmlRaw = fs.readFileSync(inputFile, 'utf8');
|
|
148
148
|
|
|
149
|
-
// 1. Mustache
|
|
149
|
+
// 1. Mustache 变量替换(根据 rawHtml 配置决定是否转义 HTML)
|
|
150
|
+
const originalEscape = Mustache.escape;
|
|
151
|
+
if (config.rawHtml) {
|
|
152
|
+
Mustache.escape = (text) => text;
|
|
153
|
+
}
|
|
150
154
|
const htmlWithVars = Mustache.render(htmlRaw, config.variables || {});
|
|
155
|
+
Mustache.escape = originalEscape;
|
|
151
156
|
|
|
152
157
|
// 2. 收集 extraCssFiles
|
|
153
158
|
const basePath = path.dirname(path.resolve(inputFile));
|
package/src/snippet.js
CHANGED
|
@@ -141,7 +141,7 @@ function buildSnippetConfig({ priorityConfigPath, cliConfigPath }) {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* 将 snippet HTML 插入到模板的 <tbody id="content"> 中。
|
|
144
|
-
* 使用深度计数算法处理嵌套 <tbody
|
|
144
|
+
* 使用深度计数算法处理嵌套 <tbody>,并自动调整片段缩进以匹配模板。
|
|
145
145
|
*/
|
|
146
146
|
function insertIntoContent(templateHtml, snippetHtml) {
|
|
147
147
|
const openRe = /<tbody[\s>][^>]*\bid\s*=\s*["']content["'][^>]*>/i;
|
|
@@ -150,6 +150,18 @@ function insertIntoContent(templateHtml, snippetHtml) {
|
|
|
150
150
|
throw new Error('模板中未找到 <tbody id="content"> 元素。模板必须包含一个带 id="content" 的 <tbody> 用于插入片段内容。');
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// 检测模板中 <tbody id="content"> 的缩进层级
|
|
154
|
+
const lineStart = templateHtml.lastIndexOf('\n', match.index) + 1;
|
|
155
|
+
const tagIndent = match.index - lineStart;
|
|
156
|
+
|
|
157
|
+
// 找到插入点后第一个非空行,确定子元素缩进
|
|
158
|
+
const afterOpen = templateHtml.substring(match.index + match[0].length);
|
|
159
|
+
const nextLineMatch = afterOpen.match(/\n(\s*)\S/);
|
|
160
|
+
const childIndent = nextLineMatch ? nextLineMatch[1].length : tagIndent + 2;
|
|
161
|
+
|
|
162
|
+
// 调整片段缩进以匹配模板
|
|
163
|
+
const reindentedSnippet = reindentHtml(snippetHtml, childIndent);
|
|
164
|
+
|
|
153
165
|
const openTagEnd = match.index + match[0].length;
|
|
154
166
|
let depth = 1;
|
|
155
167
|
let searchPos = openTagEnd;
|
|
@@ -172,7 +184,8 @@ function insertIntoContent(templateHtml, snippetHtml) {
|
|
|
172
184
|
} else {
|
|
173
185
|
depth--;
|
|
174
186
|
if (depth === 0) {
|
|
175
|
-
|
|
187
|
+
const indent = '\n' + ' '.repeat(tagIndent);
|
|
188
|
+
return templateHtml.substring(0, openTagEnd) + '\n' + reindentedSnippet + indent + templateHtml.substring(nextClose.index);
|
|
176
189
|
}
|
|
177
190
|
searchPos = nextClose.index + 1;
|
|
178
191
|
}
|
|
@@ -181,10 +194,41 @@ function insertIntoContent(templateHtml, snippetHtml) {
|
|
|
181
194
|
throw new Error('无法找到 id="content" 对应的闭合 </tbody> 标签,模板 HTML 可能存在标签不匹配问题。');
|
|
182
195
|
}
|
|
183
196
|
|
|
197
|
+
/**
|
|
198
|
+
* 重新调整 HTML 片段的缩进,使其最外层元素对齐到目标缩进层级
|
|
199
|
+
*/
|
|
200
|
+
function reindentHtml(html, targetIndent) {
|
|
201
|
+
const lines = html.split('\n');
|
|
202
|
+
|
|
203
|
+
// 找到片段中非空行的最小缩进
|
|
204
|
+
let minIndent = Infinity;
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
if (line.trim().length > 0) {
|
|
207
|
+
const indent = line.length - line.trimStart().length;
|
|
208
|
+
if (indent < minIndent) minIndent = indent;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
212
|
+
|
|
213
|
+
const delta = targetIndent - minIndent;
|
|
214
|
+
if (delta === 0) return html;
|
|
215
|
+
|
|
216
|
+
const prefix = ' '.repeat(Math.max(0, delta));
|
|
217
|
+
|
|
218
|
+
return lines.map((line) => {
|
|
219
|
+
if (line.trim().length === 0) return '';
|
|
220
|
+
if (delta > 0) {
|
|
221
|
+
return prefix + line;
|
|
222
|
+
} else {
|
|
223
|
+
return line.substring(Math.min(-delta, line.length - line.trimStart().length));
|
|
224
|
+
}
|
|
225
|
+
}).join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
// ─── 输出路径 ─────────────────────────────────────────────────────────────────
|
|
185
229
|
|
|
186
|
-
function resolveSnippetOutputPaths(
|
|
187
|
-
const name =
|
|
230
|
+
function resolveSnippetOutputPaths(outputBaseName, cwd) {
|
|
231
|
+
const name = outputBaseName;
|
|
188
232
|
return {
|
|
189
233
|
raw: path.join(cwd, name + '.raw.html'),
|
|
190
234
|
normal: path.join(cwd, name + '.html'),
|
|
@@ -202,18 +246,23 @@ function resolveSnippetOutputPaths(snippetPath, cwd) {
|
|
|
202
246
|
* 3. Juice CSS 内联 → .output.html
|
|
203
247
|
* 4. 压缩 → .minified.html
|
|
204
248
|
*/
|
|
205
|
-
async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd }) {
|
|
249
|
+
async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd, outputBaseName }) {
|
|
206
250
|
const templateHtml = fs.readFileSync(templatePath, 'utf8');
|
|
207
251
|
const snippetRaw = fs.readFileSync(snippetPath, 'utf8');
|
|
208
|
-
const outPaths = resolveSnippetOutputPaths(
|
|
209
|
-
const variables = config.variables || {};
|
|
252
|
+
const outPaths = resolveSnippetOutputPaths(outputBaseName, cwd);
|
|
253
|
+
const variables = Object.assign({}, config.variables || {});
|
|
210
254
|
|
|
211
255
|
// 1. 未渲染的片段插入模板 → .raw.html(Mustache 标签保留,无 juice)
|
|
212
|
-
const
|
|
213
|
-
fs.writeFileSync(outPaths.raw,
|
|
256
|
+
const rawMarkup = insertIntoContent(templateHtml, snippetRaw);
|
|
257
|
+
fs.writeFileSync(outPaths.raw, rawMarkup, 'utf8');
|
|
214
258
|
|
|
215
259
|
// 2. Mustache 渲染合并 HTML → .html(已渲染,无 juice 内联)
|
|
216
|
-
const
|
|
260
|
+
const originalEscape = Mustache.escape;
|
|
261
|
+
if (config.rawHtml) {
|
|
262
|
+
Mustache.escape = (text) => text;
|
|
263
|
+
}
|
|
264
|
+
const renderedHtml = Mustache.render(rawMarkup, variables);
|
|
265
|
+
Mustache.escape = originalEscape;
|
|
217
266
|
fs.writeFileSync(outPaths.normal, renderedHtml, 'utf8');
|
|
218
267
|
|
|
219
268
|
// 3. 收集模板目录的额外 CSS + Juice CSS 内联 → .output.html
|
|
@@ -239,7 +288,7 @@ async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd
|
|
|
239
288
|
` ${chalk.bold('模板:')} ${chalk.cyan(templatePath)}\n` +
|
|
240
289
|
` ${chalk.bold('配置层(低→高):')}\n${layerLines}\n` +
|
|
241
290
|
` ${chalk.bold('输出:')}\n` +
|
|
242
|
-
` ${chalk.green('·')} 原始组装 ${chalk.cyan(outPaths.raw)} ${chalk.gray('(' + fmtSize(
|
|
291
|
+
` ${chalk.green('·')} 原始组装 ${chalk.cyan(outPaths.raw)} ${chalk.gray('(' + fmtSize(rawMarkup) + ')')}\n` +
|
|
243
292
|
` ${chalk.green('·')} 已渲染 ${chalk.cyan(outPaths.normal)} ${chalk.gray('(' + fmtSize(renderedHtml) + ')')}\n` +
|
|
244
293
|
` ${chalk.green('·')} 内联后 ${chalk.cyan(outPaths.output)} ${chalk.gray('(' + fmtSize(processed) + ')')}\n` +
|
|
245
294
|
` ${chalk.green('·')} 压缩版 ${chalk.cyan(outPaths.minified)} ${chalk.gray('(' + fmtSize(minified) + ',节省 ' + savings(processed, minified) + ')')}`
|
|
@@ -357,8 +406,82 @@ async function promptTemplate(templateFiles) {
|
|
|
357
406
|
});
|
|
358
407
|
}
|
|
359
408
|
|
|
409
|
+
// ─── 输出文件名处理 ───────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 检查指定 baseName 在 cwd 下是否已有输出文件冲突
|
|
413
|
+
* 返回冲突的文件路径列表
|
|
414
|
+
*/
|
|
415
|
+
function checkOutputConflicts(baseName, cwd) {
|
|
416
|
+
const suffixes = ['.raw.html', '.html', '.output.html', '.minified.html'];
|
|
417
|
+
return suffixes
|
|
418
|
+
.map((s) => path.join(cwd, baseName + s))
|
|
419
|
+
.filter((p) => fs.existsSync(p));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 查找下一个可用版本号(baseName-v1, baseName-v2, ...)
|
|
424
|
+
*/
|
|
425
|
+
function findNextVersion(baseName, cwd) {
|
|
426
|
+
let v = 1;
|
|
427
|
+
while (true) {
|
|
428
|
+
const candidate = baseName + '-v' + v;
|
|
429
|
+
if (checkOutputConflicts(candidate, cwd).length === 0) {
|
|
430
|
+
return candidate;
|
|
431
|
+
}
|
|
432
|
+
v++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 交互式输出文件名提示
|
|
438
|
+
* 返回最终确定的 baseName
|
|
439
|
+
*/
|
|
440
|
+
async function promptOutputName(defaultBaseName, cwd) {
|
|
441
|
+
const { input, select } = await import('@inquirer/prompts');
|
|
442
|
+
|
|
443
|
+
let baseName = await input({
|
|
444
|
+
message: '请输入输出文件名:',
|
|
445
|
+
default: defaultBaseName,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// 冲突检测循环
|
|
449
|
+
while (true) {
|
|
450
|
+
const conflicts = checkOutputConflicts(baseName, cwd);
|
|
451
|
+
if (conflicts.length === 0) break;
|
|
452
|
+
|
|
453
|
+
console.log(chalk.yellow(`\n ⚠ 以下文件已存在:`));
|
|
454
|
+
conflicts.forEach((f) => console.log(chalk.gray(` ${path.basename(f)}`)));
|
|
455
|
+
|
|
456
|
+
const action = await select({
|
|
457
|
+
message: '文件已存在,请选择处理方式:',
|
|
458
|
+
choices: [
|
|
459
|
+
{ name: '覆盖现有文件', value: 'overwrite' },
|
|
460
|
+
{ name: `自动版本号(${findNextVersion(baseName, cwd)})`, value: 'version' },
|
|
461
|
+
{ name: '重新输入文件名', value: 'rename' },
|
|
462
|
+
],
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (action === 'overwrite') {
|
|
466
|
+
break;
|
|
467
|
+
} else if (action === 'version') {
|
|
468
|
+
baseName = findNextVersion(baseName, cwd);
|
|
469
|
+
console.log(chalk.green(` ✔ 已自动生成版本号:${baseName}`));
|
|
470
|
+
break;
|
|
471
|
+
} else {
|
|
472
|
+
baseName = await input({
|
|
473
|
+
message: '请重新输入输出文件名:',
|
|
474
|
+
default: baseName,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return baseName;
|
|
480
|
+
}
|
|
481
|
+
|
|
360
482
|
async function promptConfirm(summary) {
|
|
361
483
|
const { confirm } = await import('@inquirer/prompts');
|
|
484
|
+
const outPaths = resolveSnippetOutputPaths(summary.outputBaseName, summary.outputDir);
|
|
362
485
|
console.log('\n' + chalk.cyan('═══════════════════════════════════════════'));
|
|
363
486
|
console.log(chalk.bold(' 片段组装汇总'));
|
|
364
487
|
console.log(chalk.cyan('═══════════════════════════════════════════'));
|
|
@@ -369,8 +492,8 @@ async function promptConfirm(summary) {
|
|
|
369
492
|
console.log(` 配置 YAML: ${chalk.green(summary.configFile)}`);
|
|
370
493
|
console.log(chalk.gray('───────────────────────────────────────────'));
|
|
371
494
|
console.log(` 输出目录: ${chalk.cyan(summary.outputDir)}`);
|
|
495
|
+
console.log(` 输出文件名: ${chalk.green(summary.outputBaseName)}`);
|
|
372
496
|
console.log(` 输出文件:`);
|
|
373
|
-
const outPaths = resolveSnippetOutputPaths(summary.snippetPath, summary.outputDir);
|
|
374
497
|
console.log(` ${chalk.green('·')} ${path.basename(outPaths.raw)} ${chalk.gray('(未渲染,无 CSS 内联)')}`);
|
|
375
498
|
console.log(` ${chalk.green('·')} ${path.basename(outPaths.normal)} ${chalk.gray('(已渲染,无 CSS 内联)')}`);
|
|
376
499
|
console.log(` ${chalk.green('·')} ${path.basename(outPaths.output)} ${chalk.gray('(Juice CSS 内联)')}`);
|
|
@@ -392,7 +515,7 @@ async function promptConfirm(summary) {
|
|
|
392
515
|
* - 配置文件:自动检测片段目录下的 juice.yaml / juice.yml,-c 可覆盖
|
|
393
516
|
* - 合并顺序:项目默认 → 用户目录 → 片段目录配置 → CLI -c
|
|
394
517
|
*/
|
|
395
|
-
async function runSnippetMode({ snippet, template, config: cliConfigPath }) {
|
|
518
|
+
async function runSnippetMode({ snippet, template, config: cliConfigPath, outputName }) {
|
|
396
519
|
const snippetPath = path.resolve(snippet);
|
|
397
520
|
if (!fs.existsSync(snippetPath)) {
|
|
398
521
|
console.error(chalk.red(`片段文件不存在:${snippetPath}`));
|
|
@@ -437,12 +560,44 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath }) {
|
|
|
437
560
|
|
|
438
561
|
const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
|
|
439
562
|
|
|
563
|
+
// 确定输出文件名
|
|
564
|
+
const defaultBaseName = outputName || path.parse(templatePath).name;
|
|
565
|
+
let outputBaseName;
|
|
566
|
+
|
|
567
|
+
if (outputName) {
|
|
568
|
+
// 命令行指定了 --name,检查冲突,有冲突则自动版本号
|
|
569
|
+
const conflicts = checkOutputConflicts(defaultBaseName, process.cwd());
|
|
570
|
+
if (conflicts.length > 0) {
|
|
571
|
+
outputBaseName = findNextVersion(defaultBaseName, process.cwd());
|
|
572
|
+
console.warn(chalk.yellow(
|
|
573
|
+
`\n⚠ 文件冲突,自动使用版本号:${outputBaseName}`
|
|
574
|
+
));
|
|
575
|
+
} else {
|
|
576
|
+
outputBaseName = defaultBaseName;
|
|
577
|
+
}
|
|
578
|
+
} else if (template) {
|
|
579
|
+
// 非交互模式(--snippet + -f 均指定),自动版本号
|
|
580
|
+
const conflicts = checkOutputConflicts(defaultBaseName, process.cwd());
|
|
581
|
+
if (conflicts.length > 0) {
|
|
582
|
+
outputBaseName = findNextVersion(defaultBaseName, process.cwd());
|
|
583
|
+
console.warn(chalk.yellow(
|
|
584
|
+
`\n⚠ 文件冲突,自动使用版本号:${outputBaseName}`
|
|
585
|
+
));
|
|
586
|
+
} else {
|
|
587
|
+
outputBaseName = defaultBaseName;
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
// 交互模式(--snippet 单独指定),提示用户输入
|
|
591
|
+
outputBaseName = await promptOutputName(defaultBaseName, process.cwd());
|
|
592
|
+
}
|
|
593
|
+
|
|
440
594
|
await assembleSnippet({
|
|
441
595
|
snippetPath,
|
|
442
596
|
templatePath,
|
|
443
597
|
config,
|
|
444
598
|
layers,
|
|
445
599
|
cwd: process.cwd(),
|
|
600
|
+
outputBaseName,
|
|
446
601
|
});
|
|
447
602
|
}
|
|
448
603
|
|
|
@@ -493,7 +648,11 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
493
648
|
configFileName = configChoice.name;
|
|
494
649
|
}
|
|
495
650
|
|
|
496
|
-
// 6.
|
|
651
|
+
// 6. 确定输出文件名
|
|
652
|
+
const defaultBaseName = path.parse(templateChoice.path).name;
|
|
653
|
+
const outputBaseName = await promptOutputName(defaultBaseName, process.cwd());
|
|
654
|
+
|
|
655
|
+
// 7. 汇总确认
|
|
497
656
|
const confirmed = await promptConfirm({
|
|
498
657
|
brand: brand.name,
|
|
499
658
|
templateFile: templateChoice.name,
|
|
@@ -502,6 +661,7 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
502
661
|
configFile: configFileName,
|
|
503
662
|
snippetPath: snippetFile.path,
|
|
504
663
|
outputDir: process.cwd(),
|
|
664
|
+
outputBaseName,
|
|
505
665
|
});
|
|
506
666
|
|
|
507
667
|
if (!confirmed) {
|
|
@@ -509,7 +669,7 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
509
669
|
return;
|
|
510
670
|
}
|
|
511
671
|
|
|
512
|
-
//
|
|
672
|
+
// 8. 构建配置并执行
|
|
513
673
|
const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
|
|
514
674
|
|
|
515
675
|
await assembleSnippet({
|
|
@@ -518,6 +678,7 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
518
678
|
config,
|
|
519
679
|
layers,
|
|
520
680
|
cwd: process.cwd(),
|
|
681
|
+
outputBaseName,
|
|
521
682
|
});
|
|
522
683
|
}
|
|
523
684
|
|