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 +13 -6
- package/bin/juice.js +9 -2
- package/edm/elabscience/elabscience-template.html +1 -1
- package/package.json +3 -4
- package/src/context-menu.js +89 -69
- package/src/index.js +21 -8
- package/src/snippet.js +19 -11
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
|
-
├──
|
|
320
|
-
├── 🧩
|
|
321
|
-
|
|
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
|
-
|
|
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
|
|
1527
|
+
<td colspan="2">Sterilized by 0.1 μm filtration, Endotoxin Content < 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.
|
|
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
|
|
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": ">=
|
|
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
|
}
|
package/src/context-menu.js
CHANGED
|
@@ -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
|
-
* ├──
|
|
12
|
-
* ├── 🧩
|
|
13
|
-
*
|
|
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 {
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
74
|
-
|
|
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', '
|
|
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 =
|
|
133
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩
|
|
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 =
|
|
139
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '
|
|
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', '📂
|
|
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
|
-
// ──
|
|
153
|
-
const
|
|
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',
|
|
177
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
//
|
|
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',
|
|
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
|
-
` ├──
|
|
183
|
-
` ├── 🧩
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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 (
|
|
250
|
+
if (fs.existsSync(p)) return p;
|
|
232
251
|
}
|
|
233
252
|
try {
|
|
234
|
-
const result =
|
|
235
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
118
|
-
|
|
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,
|
|
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
|
-
|
|
262
|
-
|
|
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
|
});
|