juice-email-cli 2.1.9 → 2.1.12

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 CHANGED
@@ -105,8 +105,8 @@ juice
105
105
  | `--snippet <path>` | `-s` | 片段 HTML 文件路径:插入到模板 `<tbody id="content">` |
106
106
  | `--config <path>` | `-c` | 配置文件路径,不指定时自动查找 |
107
107
  | `--name <name>` | `-n` | 片段模式输出文件名(不含扩展名) |
108
- | `--install` | | 注册 Windows 右键菜单(需管理员权限) |
109
- | `--uninstall` | | 取消 Windows 右键菜单注册(需管理员权限) |
108
+ | `--install` | | 注册 Windows 右键菜单(当前用户,无需管理员) |
109
+ | `--uninstall` | | 取消 Windows 右键菜单注册 |
110
110
  | `--version` | `-v` | 查看版本号 |
111
111
  | `--help` | `-h` | 查看帮助 |
112
112
 
@@ -312,15 +312,22 @@ edm/
312
312
 
313
313
  ## Windows 右键菜单
314
314
 
315
- 注册后,在 `.html` / `.htm` 文件上右键可看到级联子菜单:
315
+ 注册后,在 `.html` / `.htm` / `.yaml` / `.yml` 文件上右键可看到统一子菜单(所有文件类型共享):
316
316
 
317
317
  ```
318
318
  📧 用 juice 生成邮件 HTML
319
- ├── 生成邮件 HTML(标准 + 压缩) → juice -f
320
- ├── 🧩 邮件片段组装(交互选择模板) juice -s
321
- └── 📂 在此目录打开 PowerShell 7 ← 仅在已安装 pwsh 时出现
319
+ ├── 📄 作为模板,生成邮件 HTML juice -f %1(后台执行)
320
+ ├── 🧩 作为片段,拼接邮件 HTML juice -s %1(交互选择模板)
321
+ ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
322
+ └── 📂 打开 PowerShell ← 仅已安装 pwsh 时出现
322
323
  ```
323
324
 
325
+ | 子命令 | 说明 |
326
+ |--------|------|
327
+ | 📄 作为模板,生成邮件 HTML | 将文件当模板处理,CSS 内联 + 变量替换 + 压缩 |
328
+ | 🧩 作为片段,拼接邮件 HTML | 将文件当片段,打开终端选择模板后拼接输出 |
329
+ | ⚙️ 作为配置,拼接邮件 HTML | 将文件当配置,打开终端选择品牌/模板/片段 |
330
+
324
331
  ```bash
325
332
  juice --install # 注册(当前用户,无需管理员)
326
333
  juice --uninstall # 卸载
package/bin/juice.js CHANGED
@@ -63,10 +63,17 @@ program
63
63
  右键菜单
64
64
  ════════════════════════════════════════════════════════════════
65
65
 
66
- .html/.htm 文件上右键即可使用:
67
- juice --install (当前用户,注册右键菜单)
66
+ 注册后,在 .html / .htm / .yaml / .yml 文件上右键即可看到:
67
+ juice --install (当前用户,无需管理员)
68
68
  juice --uninstall (卸载右键菜单)
69
69
 
70
+ 菜单结构(所有文件类型统一):
71
+ 📧 用 juice 生成邮件 HTML
72
+ ├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
73
+ ├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
74
+ ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
75
+ └── 📂 打开 PowerShell (仅已安装 pwsh 时出现)
76
+
70
77
  更多信息:https://gitee.com/siriussupreme/juice-cli
71
78
  `)
72
79
  .action(async (options) => {
@@ -1524,7 +1524,7 @@
1524
1524
 
1525
1525
  <tr>
1526
1526
  <td>Quality Control</td>
1527
- <td colspan="2">Sterilized by 0.1 μm filtration, Endotoxin Content < 3 EU/mL.</td>
1527
+ <td colspan="2">Sterilized by 0.1 μm filtration, Endotoxin Content &lt; 3 EU/mL.</td>
1528
1528
  </tr>
1529
1529
  </tbody>
1530
1530
  </table>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.1.9",
3
+ "version": "2.1.12",
4
4
  "description": "CLI tool to generate email-client-compatible HTML with juice (CSS inlining) + Mustache templating + minification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/juice.js",
11
- "test": "node bin/juice.js --help",
11
+ "test": "node test/smoke.js",
12
12
  "postinstall": "node bin/juice.js --install",
13
13
  "preuninstall": "node bin/juice.js --uninstall",
14
14
  "prepare": "husky install",
@@ -46,7 +46,7 @@
46
46
  "ora": "^5.4.1"
47
47
  },
48
48
  "engines": {
49
- "node": ">=14.0.0"
49
+ "node": ">=16.0.0"
50
50
  },
51
51
  "files": [
52
52
  "bin/",
@@ -60,7 +60,6 @@
60
60
  "@commitlint/cli": "^21.0.1",
61
61
  "@commitlint/config-conventional": "^21.0.1",
62
62
  "husky": "^9.1.7",
63
- "inquirer": "^13.4.3",
64
63
  "standard-version": "^9.5.0"
65
64
  }
66
65
  }
@@ -5,19 +5,17 @@
5
5
  *
6
6
  * 使用 HKEY_CURRENT_USER,无需管理员权限,仅对当前用户生效。
7
7
  *
8
- * 菜单结构:
9
- * .html / .htm:
8
+ * 菜单结构(所有文件类型统一):
9
+ * .html / .htm / .yaml / .yml
10
10
  * 📧 用 juice 生成邮件 HTML
11
- * ├── 生成邮件 HTML(标准 + 压缩) → juice -f
12
- * ├── 🧩 邮件片段组装(交互选择模板) → juice -s
13
- * └── 📂 在此目录打开 PowerShell 7 (可选)
14
- *
15
- * .yaml / .yml:
16
- * 📧 用 juice 生成邮件 HTML
17
- * └── 📋 交互式生成邮件(使用此配置) → juice -c
11
+ * ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)
12
+ * ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)
13
+ * ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)
14
+ * └── 📂 打开 PowerShell (可选)
18
15
  */
19
16
 
20
- const { execSync } = require('child_process');
17
+ const { spawnSync } = require('child_process');
18
+ const fs = require('fs');
21
19
  const path = require('path');
22
20
  const chalk = require('chalk');
23
21
 
@@ -37,41 +35,38 @@ function getIconPath() {
37
35
 
38
36
  // ─── 注册表操作封装 ───────────────────────────────────────────────────────────
39
37
 
38
+ const isWindows = process.platform === 'win32';
39
+
40
40
  /**
41
- * 执行 reg add,失败时打印警告而不中断
41
+ * 执行 reg add — 使用 spawnSync 直接调用 reg.exe,避免 cmd.exe 解析特殊字符
42
42
  */
43
43
  function regAdd(key, valueName, type, data) {
44
- const vFlag = valueName === '' ? '/ve' : `/v "${valueName}"`;
45
- const tFlag = type ? `/t ${type}` : '';
46
- const dFlag = data !== undefined ? `/d "${escapeRegData(data)}"` : '';
47
- const cmd = `reg add "${key}" ${vFlag} ${tFlag} ${dFlag} /f`.replace(/\s+/g, ' ').trim();
48
- try {
49
- execSync(cmd, { stdio: 'pipe' });
50
- return true;
51
- } catch (e) {
52
- const msg = e.stderr ? e.stderr.toString().trim() : e.message;
53
- console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
54
- return false;
44
+ if (!isWindows) return false;
45
+ const args = ['add', key, '/f'];
46
+ if (valueName === '') {
47
+ args.push('/ve');
48
+ } else {
49
+ args.push('/v', valueName);
55
50
  }
56
- }
51
+ if (type) args.push('/t', type);
52
+ if (data !== undefined) args.push('/d', data);
57
53
 
58
- /**
59
- * 执行 reg delete,键不存在时静默忽略,返回是否实际删除了
60
- */
61
- function regDelete(key) {
62
- try {
63
- execSync(`reg delete "${key}" /f`, { stdio: 'pipe' });
64
- return true;
65
- } catch (_) {
54
+ const result = spawnSync('reg', args, { stdio: 'pipe' });
55
+ if (result.status !== 0) {
56
+ const msg = (result.stderr || '').toString().trim();
57
+ console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
66
58
  return false;
67
59
  }
60
+ return true;
68
61
  }
69
62
 
70
63
  /**
71
- * 转义注册表数据中的特殊字符
64
+ * 执行 reg delete — 键不存在时静默忽略
72
65
  */
73
- function escapeRegData(str) {
74
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
66
+ function regDelete(key) {
67
+ if (!isWindows) return false;
68
+ const result = spawnSync('reg', ['delete', key, '/f'], { stdio: 'pipe' });
69
+ return result.status === 0;
75
70
  }
76
71
 
77
72
  // ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
@@ -102,18 +97,38 @@ const SUBCMDS = {
102
97
  pwsh: 'JuiceEmail.OpenPwsh',
103
98
  };
104
99
 
105
- // 旧版本可能残留的子命令名(升级时清理)
106
- const LEGACY_SUBCMDS = [
107
- 'JuiceEmail.Generate',
108
- 'JuiceEmail.Snippet',
109
- 'JuiceEmail.OpenPwsh',
110
- ];
111
-
112
100
  const PARENT_KEY_NAME = 'JuiceEmail';
113
101
 
102
+ /**
103
+ * 为交互式命令包一层 PowerShell:
104
+ * - 成功:窗口自动关闭
105
+ * - 失败:窗口保持开启,显示重新执行的命令
106
+ */
107
+ function wrapInteractive(nodePath, scriptPath, cliArgs) {
108
+ const node = nodePath.replace(/'/g, "''");
109
+ const script = scriptPath.replace(/'/g, "''");
110
+ // %1 由 Windows 在 CreateProcess 前展开为实际文件路径
111
+ const ps = [
112
+ `& '${node}' '${script}' ${cliArgs}`,
113
+ `if ($LASTEXITCODE) {`,
114
+ ` Write-Host ''`,
115
+ ` Write-Host '[Failed] Exit code:' $LASTEXITCODE`,
116
+ ` Write-Host '[Re-run] juice ${cliArgs}'`,
117
+ ` Write-Host ''`,
118
+ ` Read-Host 'Press Enter to close'`,
119
+ `}`,
120
+ ].join('; ');
121
+ return `powershell.exe -Command "${ps}"`;
122
+ }
123
+
114
124
  // ─── 注册 ─────────────────────────────────────────────────────────────────────
115
125
 
116
126
  async function registerContextMenu() {
127
+ if (!isWindows) {
128
+ console.log(chalk.gray(' 右键菜单仅支持 Windows,已跳过。\n'));
129
+ return;
130
+ }
131
+
117
132
  console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
118
133
 
119
134
  const nodePath = getNodePath();
@@ -122,21 +137,21 @@ async function registerContextMenu() {
122
137
 
123
138
  let ok = true;
124
139
 
125
- // ── 子命令 1:普通模式 - juice 生成 ──────────────────────────────────────
140
+ // ── 子命令 1:普通模式 - juice 生成(非交互,不需终端)─────────────────
126
141
  const generateCmd = `"${nodePath}" "${scriptPath}" -f %1`;
127
- ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', ' 生成邮件 HTML(标准 + 压缩)') && ok;
142
+ ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', '📄 作为模板,生成邮件 HTML') && ok;
128
143
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, 'Icon', 'REG_SZ', iconPath);
129
144
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}\\command`, '', 'REG_SZ', generateCmd);
130
145
 
131
- // ── 子命令 2:片段模式 - 片段组装 ────────────────────────────────────────
132
- const snippetCmd = `"${nodePath}" "${scriptPath}" -s %1`;
133
- ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 邮件片段组装(交互选择模板)') && ok;
146
+ // ── 子命令 2:片段模式 - 片段组装(交互,需要终端)────────────────────────
147
+ const snippetCmd = wrapInteractive(nodePath, scriptPath, '-s %1');
148
+ ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 作为片段,拼接邮件 HTML') && ok;
134
149
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, 'Icon', 'REG_SZ', iconPath);
135
150
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}\\command`, '', 'REG_SZ', snippetCmd);
136
151
 
137
- // ── 子命令 3:配置文件模式 - 交互式生成 ──────────────────────────────────
138
- const configCmd = `"${nodePath}" "${scriptPath}" -c %1`;
139
- ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '📋 交互式生成邮件(使用此配置)') && ok;
152
+ // ── 子命令 3:配置文件模式 - 交互式生成(交互,需要终端)──────────────────
153
+ const configCmd = wrapInteractive(nodePath, scriptPath, '-c %1');
154
+ ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '⚙️ 作为配置,拼接邮件 HTML') && ok;
140
155
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, 'Icon', 'REG_SZ', iconPath);
141
156
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}\\command`, '', 'REG_SZ', configCmd);
142
157
 
@@ -144,29 +159,30 @@ async function registerContextMenu() {
144
159
  const pwshPath = resolvePwsh();
145
160
  if (pwshPath) {
146
161
  const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`;
147
- ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 在此目录打开 PowerShell 7') && ok;
162
+ ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 打开 PowerShell') && ok;
148
163
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, 'Icon', 'REG_SZ', pwshPath);
149
164
  regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}\\command`, '', 'REG_SZ', pwshCmd);
150
165
  }
151
166
 
152
- // ── 注册 HTML 父菜单(二级子菜单)────────────────────────────────────────
153
- const htmlSubs = pwshPath
154
- ? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.pwsh}`
155
- : `${SUBCMDS.generate};${SUBCMDS.snippet}`;
167
+ // ── 所有文件类型共用同一套子命令 ────────────────────────────────────────
168
+ const allSubs = pwshPath
169
+ ? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config};${SUBCMDS.pwsh}`
170
+ : `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config}`;
156
171
 
172
+ // .html / .htm
157
173
  for (const root of HTML_ROOTS) {
158
174
  const parentKey = `${root}\\${PARENT_KEY_NAME}`;
159
175
  ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
160
176
  regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
161
- regAdd(parentKey, 'SubCommands', 'REG_SZ', htmlSubs);
177
+ regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
162
178
  }
163
179
 
164
- // ── 注册 YAML 父菜单(二级子菜单)────────────────────────────────────────
180
+ // .yaml / .yml(与 .html 共享同一套子命令)
165
181
  for (const root of YAML_ROOTS) {
166
182
  const parentKey = `${root}\\${PARENT_KEY_NAME}`;
167
183
  ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
168
184
  regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
169
- regAdd(parentKey, 'SubCommands', 'REG_SZ', SUBCMDS.config);
185
+ regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
170
186
  }
171
187
 
172
188
  // ── 输出 ──────────────────────────────────────────────────────────────────
@@ -177,13 +193,12 @@ async function registerContextMenu() {
177
193
  console.log(
178
194
  '\n' +
179
195
  chalk.green(' ✔ 右键菜单注册完成!') + '\n\n' +
180
- ` ${chalk.bold('.html / .htm')} 文件右键:\n` +
196
+ ` ${chalk.bold('.html / .htm / .yaml / .yml')} 文件右键:\n` +
181
197
  ` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
182
- ` ├── 生成邮件 HTML(标准 + 压缩) → juice -f\n` +
183
- ` ├── 🧩 邮件片段组装(交互选择模板) → juice -s\n` +
184
- (pwshPath ? ` └── 📂 在此目录打开 PowerShell 7\n` : '') +
185
- `\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
186
- ` └── 📧 交互式生成邮件(使用此配置) → juice -c\n` +
198
+ ` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
199
+ ` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
200
+ ` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
201
+ (pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
187
202
  '\n' +
188
203
  chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
189
204
  );
@@ -192,6 +207,11 @@ async function registerContextMenu() {
192
207
  // ─── 取消注册 ─────────────────────────────────────────────────────────────────
193
208
 
194
209
  async function unregisterContextMenu() {
210
+ if (!isWindows) {
211
+ console.log(chalk.gray(' 右键菜单仅支持 Windows,已跳过。\n'));
212
+ return;
213
+ }
214
+
195
215
  console.log(chalk.cyan('\n 取消注册 juice 右键菜单...\n'));
196
216
 
197
217
  let removed = 0;
@@ -206,9 +226,8 @@ async function unregisterContextMenu() {
206
226
  if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
207
227
  }
208
228
 
209
- // 清理子命令(当前版本 + 旧版本残留)
210
- const allSubcmds = [...new Set([...Object.values(SUBCMDS), ...LEGACY_SUBCMDS])];
211
- for (const name of allSubcmds) {
229
+ // 清理子命令
230
+ for (const name of Object.values(SUBCMDS)) {
212
231
  regDelete(`${SUBCMD_SPACE}\\${name}`);
213
232
  }
214
233
 
@@ -228,11 +247,12 @@ function resolvePwsh() {
228
247
  path.join(process.env['LOCALAPPDATA'] || '', 'Microsoft', 'PowerShell', 'pwsh.exe'),
229
248
  ];
230
249
  for (const p of candidates) {
231
- if (require('fs').existsSync(p)) return p;
250
+ if (fs.existsSync(p)) return p;
232
251
  }
233
252
  try {
234
- const result = execSync('where pwsh', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
235
- if (result && require('fs').existsSync(result)) return result;
253
+ const result = spawnSync('where', ['pwsh'], { stdio: 'pipe' });
254
+ const line = result.stdout.toString().trim().split('\n')[0].trim();
255
+ if (line && fs.existsSync(line)) return line;
236
256
  } catch (_) {}
237
257
  return null;
238
258
  }
package/src/index.js CHANGED
@@ -33,6 +33,10 @@ const HOME_CANDIDATES = [
33
33
  path.join(os.homedir(), 'juice.yml'),
34
34
  ];
35
35
 
36
+ function resolveHomeConfig() {
37
+ return HOME_CANDIDATES.find((c) => fs.existsSync(c)) || null;
38
+ }
39
+
36
40
  // ─── 配置文件查找 ─────────────────────────────────────────────────────────────
37
41
 
38
42
  /**
@@ -73,7 +77,7 @@ function findConfigs(configPath, inputFile) {
73
77
  }
74
78
  }
75
79
 
76
- return { highPriorityPath, homePath: HOME_CANDIDATES[0] };
80
+ return { highPriorityPath, homePath: resolveHomeConfig() };
77
81
  }
78
82
 
79
83
  // ─── 配置文件加载 ─────────────────────────────────────────────────────────────
@@ -121,14 +125,13 @@ function deepMerge(base, ...overrides) {
121
125
  * CLI 内置默认值 < 用户目录 ~/juice.yaml < 优先配置(-c/输入目录)
122
126
  */
123
127
  function buildConfig(highPriorityPath, homePath) {
124
- const homeExists = fs.existsSync(homePath);
125
128
  const layers = [];
126
129
 
127
130
  // 1. CLI 内置默认值(最低优先级)
128
131
  layers.push({ label: 'CLI 内置默认值', data: CODE_DEFAULTS });
129
132
 
130
133
  // 2. 用户目录配置(如果有)
131
- if (homeExists) {
134
+ if (homePath) {
132
135
  layers.push({ label: `用户目录配置 (${homePath})`, data: loadYaml(homePath) });
133
136
  }
134
137
 
@@ -148,11 +151,15 @@ function processTemplate(inputFile, config) {
148
151
 
149
152
  // 1. Mustache 变量替换(根据 rawHtml 配置决定是否转义 HTML)
150
153
  const originalEscape = Mustache.escape;
151
- if (config.rawHtml) {
152
- Mustache.escape = (text) => text;
154
+ let htmlWithVars;
155
+ try {
156
+ if (config.rawHtml) {
157
+ Mustache.escape = (text) => text;
158
+ }
159
+ htmlWithVars = Mustache.render(htmlRaw, config.variables || {});
160
+ } finally {
161
+ Mustache.escape = originalEscape;
153
162
  }
154
- const htmlWithVars = Mustache.render(htmlRaw, config.variables || {});
155
- Mustache.escape = originalEscape;
156
163
 
157
164
  // 2. 收集 extraCssFiles
158
165
  const basePath = path.dirname(path.resolve(inputFile));
@@ -228,10 +235,16 @@ async function run({ file, config: configPath }) {
228
235
 
229
236
  // 5. 写出标准版
230
237
  spinner.text = '写出 .output.html ...';
238
+ if (fs.existsSync(outPaths.normal)) {
239
+ spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.normal}`));
240
+ }
231
241
  fs.writeFileSync(outPaths.normal, resultHtml, encoding);
232
242
 
233
243
  // 6. 压缩并写出
234
244
  spinner.text = '写出 .minified.html ...';
245
+ if (fs.existsSync(outPaths.minified)) {
246
+ spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.minified}`));
247
+ }
235
248
  const minified = await minifyHtml(resultHtml, config.minify);
236
249
  fs.writeFileSync(outPaths.minified, minified, encoding);
237
250
 
@@ -266,4 +279,4 @@ function savings(original, minified) {
266
279
  return (((orig - mini) / orig) * 100).toFixed(1) + '%';
267
280
  }
268
281
 
269
- module.exports = { run, findConfigs, buildConfig, processTemplate, minifyHtml, deepMerge, collectExtraCss, loadYaml, fmtSize, savings };
282
+ module.exports = { run, findConfigs, buildConfig, processTemplate, minifyHtml, deepMerge, collectExtraCss, loadYaml, fmtSize, savings, DEFAULT_CONFIG_PATH, resolveHomeConfig };
package/src/snippet.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  minifyHtml,
14
14
  fmtSize,
15
15
  savings,
16
+ DEFAULT_CONFIG_PATH,
16
17
  } = require('./index');
17
18
 
18
19
  // ─── EDM 目录解析 ──────────────────────────────────────────────────────────────
@@ -95,8 +96,6 @@ function getBrand(filePath, edmDir) {
95
96
 
96
97
  // ─── 配置合并 ─────────────────────────────────────────────────────────────────
97
98
 
98
- const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', 'defaults', 'juice.yaml');
99
-
100
99
  /**
101
100
  * 片段模式配置合并
102
101
  *
@@ -114,8 +113,12 @@ function buildSnippetConfig({ priorityConfigPath, cliConfigPath }) {
114
113
  layers.push({ label: 'CLI 内置默认值', data: defaults });
115
114
 
116
115
  // 2. 用户主目录
117
- const homePath = path.join(os.homedir(), 'juice.yaml');
118
- if (fs.existsSync(homePath)) {
116
+ const homeCandidates = [
117
+ path.join(os.homedir(), 'juice.yaml'),
118
+ path.join(os.homedir(), 'juice.yml'),
119
+ ];
120
+ const homePath = homeCandidates.find((c) => fs.existsSync(c));
121
+ if (homePath) {
119
122
  layers.push({ label: `用户目录配置 (${homePath})`, data: loadYaml(homePath) });
120
123
  }
121
124
 
@@ -246,7 +249,7 @@ function resolveSnippetOutputPaths(outputBaseName, cwd) {
246
249
  * 3. Juice CSS 内联 → .output.html
247
250
  * 4. 压缩 → .minified.html
248
251
  */
249
- async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd, outputBaseName }) {
252
+ async function assembleSnippet({ snippetPath, templatePath, config, cwd, outputBaseName }) {
250
253
  const templateHtml = fs.readFileSync(templatePath, 'utf8');
251
254
  const snippetRaw = fs.readFileSync(snippetPath, 'utf8');
252
255
  const outPaths = resolveSnippetOutputPaths(outputBaseName, cwd);
@@ -258,11 +261,15 @@ async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd,
258
261
 
259
262
  // 2. Mustache 渲染合并 HTML → .html(已渲染,无 juice 内联)
260
263
  const originalEscape = Mustache.escape;
261
- if (config.rawHtml) {
262
- Mustache.escape = (text) => text;
264
+ let renderedHtml;
265
+ try {
266
+ if (config.rawHtml) {
267
+ Mustache.escape = (text) => text;
268
+ }
269
+ renderedHtml = Mustache.render(rawMarkup, variables);
270
+ } finally {
271
+ Mustache.escape = originalEscape;
263
272
  }
264
- const renderedHtml = Mustache.render(rawMarkup, variables);
265
- Mustache.escape = originalEscape;
266
273
  fs.writeFileSync(outPaths.normal, renderedHtml, 'utf8');
267
274
 
268
275
  // 3. 收集模板目录的额外 CSS + Juice CSS 内联 → .output.html
@@ -278,6 +285,7 @@ async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd,
278
285
  fs.writeFileSync(outPaths.minified, minified, 'utf8');
279
286
 
280
287
  // 5. 报告
288
+ const layers = config._layers || [];
281
289
  const layerLines = layers
282
290
  .map((l) => ` ${chalk.gray('·')} ${l.label}`)
283
291
  .join('\n');
@@ -585,6 +593,7 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
585
593
  }
586
594
 
587
595
  const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
596
+ config._layers = layers;
588
597
 
589
598
  // 确定输出文件名
590
599
  const defaultBaseName = outputName || path.parse(templatePath).name;
@@ -621,7 +630,6 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
621
630
  snippetPath,
622
631
  templatePath,
623
632
  config,
624
- layers,
625
633
  cwd: process.cwd(),
626
634
  outputBaseName,
627
635
  });
@@ -697,12 +705,12 @@ async function runInteractiveMode({ config: cliConfigPath }) {
697
705
 
698
706
  // 8. 构建配置并执行
699
707
  const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
708
+ config._layers = layers;
700
709
 
701
710
  await assembleSnippet({
702
711
  snippetPath: snippetFile.path,
703
712
  templatePath: templateChoice.path,
704
713
  config,
705
- layers,
706
714
  cwd: process.cwd(),
707
715
  outputBaseName,
708
716
  });