juice-email-cli 2.3.14 → 2.4.0

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/bin/juice.js CHANGED
@@ -1,34 +1,36 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
2
+ import { Command } from 'commander';
3
+ import { readFileSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, resolve } from 'path';
6
+ import chalk from 'chalk';
7
+ import { run } from '../src/index.js';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
3
11
 
4
- const { program } = require('commander');
5
- const path = require('path');
6
- const pkg = require('../package.json');
7
- const { run } = require('../src/index');
8
12
  let ExitPromptError;
9
13
  try {
10
- ({ ExitPromptError } = require('@inquirer/core'));
14
+ ({ ExitPromptError } = await import('@inquirer/core'));
11
15
  } catch (_) {}
12
16
 
13
- /**
14
- * Wrap an async action handler to catch Ctrl+C and other errors gracefully.
15
- */
16
17
  function safeAction(fn) {
17
18
  return async (...args) => {
18
19
  try {
19
20
  await fn(...args);
20
21
  } catch (err) {
21
22
  if (ExitPromptError && err instanceof ExitPromptError) {
22
- // Ctrl+C — exit silently
23
23
  process.exit(0);
24
24
  }
25
- console.error('\n' + require('chalk').red(` ✘ ${err.message}`));
25
+ console.error('\n' + chalk.red(` ✘ ${err.message}`));
26
26
  if (process.env.DEBUG) console.error(err);
27
27
  process.exit(1);
28
28
  }
29
29
  };
30
30
  }
31
31
 
32
+ const program = new Command();
33
+
32
34
  program
33
35
  .name('juice')
34
36
  .description('生成符合各大邮件平台的内联 CSS HTML 邮件(同时输出标准版 + 压缩版)')
@@ -68,6 +70,24 @@ program
68
70
  交互模式(逐步选择品牌、模板、片段、配置):
69
71
  juice
70
72
 
73
+ ════════════════════════════════════════════════════════════════
74
+ 资源浏览
75
+ ════════════════════════════════════════════════════════════════
76
+
77
+ 查看资源树:
78
+ juice view 查看所有品牌、模板、系列、片段
79
+ juice view elabscience 查看指定品牌的模板和系列
80
+ juice view --templates 列出所有模板
81
+ juice view --series 列出所有系列
82
+ juice view --snippets 列出所有片段
83
+
84
+ 拷贝到当前目录:
85
+ juice init 交互式选择并拷贝模板/片段/配置
86
+ juice init --template <file> 仅拷贝指定模板 HTML
87
+ juice init --snippet <file> 仅拷贝指定片段 HTML
88
+ juice init --config <file> 仅拷贝指定配置 YAML
89
+ juice init --all [target] 拷贝整个 EDM 资源库
90
+
71
91
  ════════════════════════════════════════════════════════════════
72
92
  输出文件说明
73
93
  ════════════════════════════════════════════════════════════════
@@ -111,7 +131,7 @@ program
111
131
  .option('--series', '列出所有系列')
112
132
  .option('--snippets', '列出所有片段')
113
133
  .action(safeAction(async (viewPath, options) => {
114
- const { runViewMode } = require('../src/view');
134
+ const { runViewMode } = await import('../src/view.js');
115
135
  await runViewMode({
116
136
  viewPath: viewPath || null,
117
137
  interactive: !!options.interactive,
@@ -130,7 +150,7 @@ program
130
150
  .option('-c, --config <file-path>', '仅拷贝配置 YAML 文件')
131
151
  .option('--all [target]', '拷贝整个 EDM 资源目录到当前或指定目录')
132
152
  .action(safeAction(async (initPath, options) => {
133
- const { runInitMode } = require('../src/init');
153
+ const { runInitMode } = await import('../src/init.js');
134
154
  await runInitMode({
135
155
  initPath: initPath || null,
136
156
  template: options.template || null,
@@ -143,21 +163,19 @@ program
143
163
  // ─── Default action (backward compatible) ───────────────────────────────
144
164
  program
145
165
  .action(safeAction(async (options) => {
146
- // 注册/卸载右键菜单
147
166
  if (options.install) {
148
- const { registerContextMenu } = require('../src/context-menu');
167
+ const { registerContextMenu } = await import('../src/context-menu.js');
149
168
  await registerContextMenu();
150
169
  return;
151
170
  }
152
171
  if (options.uninstall) {
153
- const { unregisterContextMenu } = require('../src/context-menu');
172
+ const { unregisterContextMenu } = await import('../src/context-menu.js');
154
173
  await unregisterContextMenu();
155
174
  return;
156
175
  }
157
176
 
158
- // --snippet / -s 模式:片段组装
159
177
  if (options.snippet) {
160
- const { runSnippetMode } = require('../src/snippet');
178
+ const { runSnippetMode } = await import('../src/snippet.js');
161
179
  await runSnippetMode({
162
180
  snippet: options.snippet,
163
181
  template: options.file,
@@ -167,7 +185,6 @@ program
167
185
  return;
168
186
  }
169
187
 
170
- // 普通模式:-f 指定文件
171
188
  let inputFile = options.file;
172
189
  if (!inputFile && program.args.length > 0) {
173
190
  inputFile = program.args[0];
@@ -181,10 +198,8 @@ program
181
198
  return;
182
199
  }
183
200
 
184
- // 全交互模式:无 --snippet,无 -f
185
- const { runInteractiveMode } = require('../src/snippet');
201
+ const { runInteractiveMode } = await import('../src/snippet.js');
186
202
  await runInteractiveMode({ config: options.config });
187
203
  }));
188
204
 
189
- program.allowUnknownOption(false);
190
205
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.3.14",
3
+ "version": "2.4.0",
4
+ "type": "module",
4
5
  "description": "CLI tool to generate email-client-compatible HTML with juice (CSS inlining) + Mustache templating + minification",
5
6
  "main": "src/index.js",
6
7
  "bin": {
@@ -36,17 +37,17 @@
36
37
  "url": "https://github.com/GuoSirius/juice-cli/issues"
37
38
  },
38
39
  "dependencies": {
39
- "@inquirer/prompts": "^5.0.2",
40
- "chalk": "^4.1.2",
41
- "commander": "^11.1.0",
40
+ "@inquirer/prompts": "^8.4.3",
41
+ "chalk": "^5.6.2",
42
+ "commander": "^14.0.3",
42
43
  "html-minifier-terser": "^7.2.0",
43
- "js-yaml": "^4.1.0",
44
- "juice": "^10.0.0",
44
+ "js-yaml": "^4.1.1",
45
+ "juice": "^11.1.1",
45
46
  "mustache": "^4.2.0",
46
- "ora": "^5.4.1"
47
+ "ora": "^9.4.0"
47
48
  },
48
49
  "engines": {
49
- "node": ">=16.0.0"
50
+ "node": ">=24.0.0"
50
51
  },
51
52
  "files": [
52
53
  "bin/",
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Windows 右键菜单注册 / 取消注册
5
3
  *
@@ -32,10 +30,13 @@
32
30
  * └── 📂 在此打开终端 (可选)
33
31
  */
34
32
 
35
- const { spawnSync } = require('child_process');
36
- const fs = require('fs');
37
- const path = require('path');
38
- const chalk = require('chalk');
33
+ import { spawnSync } from 'child_process';
34
+ import fs from 'fs';
35
+ import path from 'path';
36
+ import { fileURLToPath } from 'url';
37
+ import chalk from 'chalk';
38
+
39
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
40
 
40
41
  // ─── 路径工具 ─────────────────────────────────────────────────────────────────
41
42
 
@@ -459,4 +460,4 @@ async function unregisterContextMenu() {
459
460
  }
460
461
  }
461
462
 
462
- module.exports = { registerContextMenu, unregisterContextMenu };
463
+ export { registerContextMenu, unregisterContextMenu };
package/src/index.js CHANGED
@@ -1,25 +1,25 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const yaml = require('js-yaml');
7
- const juice = require('juice');
8
- const Mustache = require('mustache');
9
- const chalk = require('chalk');
10
- const ora = require('ora');
11
- const { minify: htmlMinify } = require('html-minifier-terser');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import yaml from 'js-yaml';
6
+ import juice from 'juice';
7
+ import Mustache from 'mustache';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { minify as htmlMinify } from 'html-minifier-terser';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
 
13
14
  // ─── 从 defaults/juice.yaml 加载默认配置 ─────────────────────────────────────
14
15
 
15
- const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', 'defaults', 'juice.yaml');
16
+ export const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', 'defaults', 'juice.yaml');
16
17
 
17
18
  function loadDefaultConfig() {
18
19
  try {
19
20
  const content = fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf8');
20
21
  return yaml.load(content) || {};
21
22
  } catch (e) {
22
- // 如果 defaults/juice.yaml 不存在,使用内置最小配置
23
23
  console.warn(chalk.yellow(` ⚠ 无法加载默认配置 ${DEFAULT_CONFIG_PATH},使用内置默认值\n`));
24
24
  return {};
25
25
  }
@@ -27,34 +27,20 @@ function loadDefaultConfig() {
27
27
 
28
28
  const CODE_DEFAULTS = loadDefaultConfig();
29
29
 
30
- // 用户主目录候选(支持 yaml/yml)
31
30
  const HOME_CANDIDATES = [
32
31
  path.join(os.homedir(), 'juice.yaml'),
33
32
  path.join(os.homedir(), 'juice.yml'),
34
33
  ];
35
34
 
36
- function resolveHomeConfig() {
35
+ export function resolveHomeConfig() {
37
36
  return HOME_CANDIDATES.find((c) => fs.existsSync(c)) || null;
38
37
  }
39
38
 
40
39
  // ─── 配置文件查找 ─────────────────────────────────────────────────────────────
41
40
 
42
- /**
43
- * 查找用户配置文件
44
- *
45
- * 配置加载顺序(优先级从低到高):
46
- * 1. CLI 内置默认值(defaults/juice.yaml)
47
- * 2. 用户主目录 ~/juice.yaml(如果存在)
48
- * 3. 优先配置文件(-c 指定 或 输入文件同级目录,二者互斥)
49
- *
50
- * 返回:
51
- * - highPriorityPath: 高优先级配置路径(-c 指定 或 输入文件同级目录,二者互斥)
52
- * - homePath: 用户主目录配置路径(可能不存在)
53
- */
54
- function findConfigs(configPath, inputFile) {
41
+ export function findConfigs(configPath, inputFile) {
55
42
  let highPriorityPath = null;
56
43
 
57
- // 1. 优先使用 -c 指定的配置文件
58
44
  if (configPath) {
59
45
  const resolved = path.resolve(configPath);
60
46
  if (!fs.existsSync(resolved)) {
@@ -62,7 +48,6 @@ function findConfigs(configPath, inputFile) {
62
48
  }
63
49
  highPriorityPath = resolved;
64
50
  }
65
- // 2. 否则使用输入文件同级目录下的配置(互斥)
66
51
  else if (inputFile) {
67
52
  const inputDir = path.dirname(path.resolve(inputFile));
68
53
  const fileCandidates = [
@@ -82,7 +67,7 @@ function findConfigs(configPath, inputFile) {
82
67
 
83
68
  // ─── 配置文件加载 ─────────────────────────────────────────────────────────────
84
69
 
85
- function loadYaml(filePath) {
70
+ export function loadYaml(filePath) {
86
71
  if (!filePath || !fs.existsSync(filePath)) return {};
87
72
  try {
88
73
  return yaml.load(fs.readFileSync(filePath, 'utf8')) || {};
@@ -91,10 +76,9 @@ function loadYaml(filePath) {
91
76
  }
92
77
  }
93
78
 
94
- /**
95
- * 深度合并(后面的覆盖前面的),仅对普通对象递归,数组/基本类型直接覆盖
96
- */
97
- function deepMerge(base, ...overrides) {
79
+ // ─── 深度合并 ─────────────────────────────────────────────────────────────────
80
+
81
+ export function deepMerge(base, ...overrides) {
98
82
  let result = Object.assign({}, base);
99
83
  for (const src of overrides) {
100
84
  if (!src || typeof src !== 'object' || Array.isArray(src)) continue;
@@ -118,24 +102,17 @@ function deepMerge(base, ...overrides) {
118
102
  return result;
119
103
  }
120
104
 
121
- /**
122
- * 配置合并,返回最终生效配置
123
- *
124
- * 合并顺序(优先级从低到高):
125
- * CLI 内置默认值 < 用户目录 ~/juice.yaml < 优先配置(-c/输入目录)
126
- */
127
- function buildConfig(highPriorityPath, homePath) {
105
+ // ─── 配置合并 ─────────────────────────────────────────────────────────────────
106
+
107
+ export function buildConfig(highPriorityPath, homePath) {
128
108
  const layers = [];
129
109
 
130
- // 1. CLI 内置默认值(最低优先级)
131
110
  layers.push({ label: 'CLI 内置默认值', data: CODE_DEFAULTS });
132
111
 
133
- // 2. 用户目录配置(如果有)
134
112
  if (homePath) {
135
113
  layers.push({ label: `用户目录配置 (${homePath})`, data: loadYaml(homePath) });
136
114
  }
137
115
 
138
- // 3. 优先配置(最高优先级,-c 指定 或 输入文件同级目录,互斥)
139
116
  if (highPriorityPath) {
140
117
  layers.push({ label: `优先配置 (${highPriorityPath})`, data: loadYaml(highPriorityPath) });
141
118
  }
@@ -146,10 +123,9 @@ function buildConfig(highPriorityPath, homePath) {
146
123
 
147
124
  // ─── HTML 模板处理 ────────────────────────────────────────────────────────────
148
125
 
149
- function processTemplate(inputFile, config) {
126
+ export function processTemplate(inputFile, config) {
150
127
  const htmlRaw = fs.readFileSync(inputFile, 'utf8');
151
128
 
152
- // 1. Mustache 变量替换(根据 rawHtml 配置决定是否转义 HTML)
153
129
  const originalEscape = Mustache.escape;
154
130
  let htmlWithVars;
155
131
  try {
@@ -161,18 +137,16 @@ function processTemplate(inputFile, config) {
161
137
  Mustache.escape = originalEscape;
162
138
  }
163
139
 
164
- // 2. 收集 extraCssFiles
165
140
  const basePath = path.dirname(path.resolve(inputFile));
166
141
  const extraCss = collectExtraCss(basePath, config);
167
142
 
168
- // 3. juice CSS 内联
169
143
  const juiceOpts = Object.assign({}, config.juice || {});
170
144
  delete juiceOpts.extraCssFiles;
171
145
 
172
146
  return juice(htmlWithVars, { ...juiceOpts, extraCss });
173
147
  }
174
148
 
175
- function collectExtraCss(basePath, config) {
149
+ export function collectExtraCss(basePath, config) {
176
150
  const extraFiles = (config.juice && config.juice.extraCssFiles) || [];
177
151
  if (!extraFiles.length) return '';
178
152
  return extraFiles
@@ -189,7 +163,7 @@ function collectExtraCss(basePath, config) {
189
163
 
190
164
  // ─── 压缩 ─────────────────────────────────────────────────────────────────────
191
165
 
192
- async function minifyHtml(html, minifyConfig) {
166
+ export async function minifyHtml(html, minifyConfig) {
193
167
  return htmlMinify(html, {
194
168
  removeConditionalComments: false,
195
169
  ...(minifyConfig || {}),
@@ -208,39 +182,46 @@ function resolveOutputPaths(inputFile, config) {
208
182
  };
209
183
  }
210
184
 
185
+ // ─── 格式化 ───────────────────────────────────────────────────────────────────
186
+
187
+ export function fmtSize(str) {
188
+ const b = Buffer.byteLength(str, 'utf8');
189
+ return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
190
+ }
191
+
192
+ export function savings(original, minified) {
193
+ const orig = Buffer.byteLength(original, 'utf8');
194
+ const mini = Buffer.byteLength(minified, 'utf8');
195
+ return (((orig - mini) / orig) * 100).toFixed(1) + '%';
196
+ }
197
+
211
198
  // ─── 主入口 ───────────────────────────────────────────────────────────────────
212
199
 
213
- async function run({ file, config: configPath }) {
200
+ export async function run({ file, config: configPath }) {
214
201
  const spinner = ora({ text: '正在处理...', color: 'cyan' }).start();
215
202
 
216
203
  try {
217
- // 1. 输入文件
218
204
  const inputFile = path.resolve(file);
219
205
  if (!fs.existsSync(inputFile)) {
220
206
  spinner.fail(chalk.red(`输入文件不存在:${inputFile}`));
221
207
  process.exit(1);
222
208
  }
223
209
 
224
- // 2. 查找配置(-c 和输入文件同级目录互斥)+ 合并
225
210
  const { highPriorityPath, homePath } = findConfigs(configPath, inputFile);
226
211
  const { config, layers } = buildConfig(highPriorityPath, homePath);
227
212
  const encoding = (config.output && config.output.encoding) || 'utf8';
228
213
 
229
- // 3. CSS 内联
230
214
  spinner.text = `CSS 内联处理:${path.basename(inputFile)}`;
231
215
  const resultHtml = processTemplate(inputFile, config);
232
216
 
233
- // 4. 输出路径
234
217
  const outPaths = resolveOutputPaths(inputFile, config);
235
218
 
236
- // 5. 写出标准版
237
219
  spinner.text = '写出 .output.html ...';
238
220
  if (fs.existsSync(outPaths.normal)) {
239
221
  spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.normal}`));
240
222
  }
241
223
  fs.writeFileSync(outPaths.normal, resultHtml, encoding);
242
224
 
243
- // 6. 压缩并写出
244
225
  spinner.text = '写出 .minified.html ...';
245
226
  if (fs.existsSync(outPaths.minified)) {
246
227
  spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.minified}`));
@@ -248,7 +229,6 @@ async function run({ file, config: configPath }) {
248
229
  const minified = await minifyHtml(resultHtml, config.minify);
249
230
  fs.writeFileSync(outPaths.minified, minified, encoding);
250
231
 
251
- // 7. 汇报
252
232
  const layerLines = layers
253
233
  .map((l) => ` ${chalk.gray('·')} ${l.label}`)
254
234
  .join('\n');
@@ -267,16 +247,3 @@ async function run({ file, config: configPath }) {
267
247
  process.exit(1);
268
248
  }
269
249
  }
270
-
271
- function fmtSize(str) {
272
- const b = Buffer.byteLength(str, 'utf8');
273
- return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
274
- }
275
-
276
- function savings(original, minified) {
277
- const orig = Buffer.byteLength(original, 'utf8');
278
- const mini = Buffer.byteLength(minified, 'utf8');
279
- return (((orig - mini) / orig) * 100).toFixed(1) + '%';
280
- }
281
-
282
- module.exports = { run, findConfigs, buildConfig, processTemplate, minifyHtml, deepMerge, collectExtraCss, loadYaml, fmtSize, savings, DEFAULT_CONFIG_PATH, resolveHomeConfig };
package/src/init.js CHANGED
@@ -1,13 +1,11 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const chalk = require('chalk');
6
- const {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import {
7
5
  resolveEdmDir, loadMeta, findBrands, findTemplateVersions,
8
- findSeriesDirs, findSnippetVariants, findConfigs,
6
+ findSeriesDirs, filterSeries, findSnippetVariants, findConfigs,
9
7
  promptOutputName,
10
- } = require('./snippet');
8
+ } from './snippet.js';
11
9
 
12
10
  function fmtBytes(b) {
13
11
  return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
@@ -92,6 +90,7 @@ async function selectWithNav(message, choices, showBack) {
92
90
  async function interactiveInit(edmDir, cwd) {
93
91
  let step = 'brand';
94
92
  let brand = null, version = null, series = null, variant = null;
93
+ let hadVariantStep = false; // track if variant step was actually shown
95
94
 
96
95
  while (true) {
97
96
  if (step === 'brand') {
@@ -122,7 +121,9 @@ async function interactiveInit(edmDir, cwd) {
122
121
  }
123
122
 
124
123
  if (step === 'series') {
125
- const allSeries = findSeriesDirs(brand.path);
124
+ let allSeries = findSeriesDirs(brand.path);
125
+ // Apply template version series filtering (allow/block in _meta.yaml)
126
+ allSeries = filterSeries(allSeries, version.meta);
126
127
  if (allSeries.length === 0) {
127
128
  series = null;
128
129
  step = 'copy';
@@ -150,6 +151,7 @@ async function interactiveInit(edmDir, cwd) {
150
151
  step = 'copy';
151
152
  continue;
152
153
  }
154
+ hadVariantStep = true;
153
155
  const choices = variants.map(v => ({
154
156
  name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
155
157
  value: v,
@@ -218,7 +220,7 @@ async function interactiveInit(edmDir, cwd) {
218
220
  mainChoices,
219
221
  true
220
222
  );
221
- if (action === 'back') { step = series ? 'variant' : 'series'; break; }
223
+ if (action === 'back') { step = hadVariantStep ? 'variant' : (series ? 'series' : 'version'); break; }
222
224
  if (action === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
223
225
  if (action === 'edit') {
224
226
  const { checkbox } = await import('@inquirer/prompts');
@@ -234,7 +236,7 @@ async function interactiveInit(edmDir, cwd) {
234
236
  // fall through to confirmation below
235
237
  } else if (selected.length === 0) {
236
238
  console.log(chalk.gray('未选择任何内容。\n'));
237
- step = series ? 'variant' : 'series';
239
+ step = hadVariantStep ? 'variant' : (series ? 'series' : 'version');
238
240
  break;
239
241
  }
240
242
 
@@ -429,7 +431,7 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
429
431
  }
430
432
 
431
433
  if (initPath) {
432
- const { parseViewPath } = require('./view');
434
+ const { parseViewPath } = await import('./view.js');
433
435
  let edmDir;
434
436
  try {
435
437
  edmDir = resolveEdmDir();
@@ -522,4 +524,4 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
522
524
  await interactiveInit(edmDir, cwd);
523
525
  }
524
526
 
525
- module.exports = { runInitMode };
527
+ export { runInitMode };
package/src/snippet.js CHANGED
@@ -1,12 +1,10 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const juice = require('juice');
7
- const Mustache = require('mustache');
8
- const chalk = require('chalk');
9
- const {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import juice from 'juice';
5
+ import Mustache from 'mustache';
6
+ import chalk from 'chalk';
7
+ import {
10
8
  loadYaml,
11
9
  deepMerge,
12
10
  collectExtraCss,
@@ -14,7 +12,7 @@ const {
14
12
  fmtSize,
15
13
  savings,
16
14
  DEFAULT_CONFIG_PATH,
17
- } = require('./index');
15
+ } from './index.js';
18
16
 
19
17
  // ─── EDM 目录解析 ──────────────────────────────────────────────────────────────
20
18
 
@@ -204,24 +202,6 @@ function findConfigs(variantDir, sourceLabel) {
204
202
  * Collect configs from multiple directories, deduplicating by path.
205
203
  * Returns { name, path, isOptimal, source }[].
206
204
  */
207
- function collectConfigs(dirs) {
208
- const seen = new Set();
209
- const result = [];
210
- for (const { dir, label, markOptimal } of dirs) {
211
- if (!fs.existsSync(dir)) continue;
212
- const configs = findConfigs(dir, label);
213
- for (const c of configs) {
214
- if (seen.has(c.path)) continue;
215
- seen.add(c.path);
216
- if (markOptimal && c.name === 'juice.yaml') {
217
- c.isOptimal = true;
218
- }
219
- result.push(c);
220
- }
221
- }
222
- return result;
223
- }
224
-
225
205
  function findFiles(dir, regex) {
226
206
  const entries = fs.readdirSync(dir, { withFileTypes: true });
227
207
  return entries
@@ -425,11 +405,11 @@ async function assembleSnippet({ snippetPath, templatePath, config, cwd, outputB
425
405
  const outPaths = resolveSnippetOutputPaths(outputBaseName, cwd);
426
406
  const variables = Object.assign({}, config.variables || {});
427
407
 
428
- // 1. 未渲染的片段插入模板 → .raw.html(Mustache 标签保留,无 juice)
408
+ // 1. 未渲染的片段插入模板 → .raw.html
429
409
  const rawMarkup = insertIntoContent(templateHtml, snippetRaw);
430
410
  fs.writeFileSync(outPaths.raw, rawMarkup, 'utf8');
431
411
 
432
- // 2. Mustache 渲染合并 HTML → .html(已渲染,无 juice 内联)
412
+ // 2. Mustache 渲染 → .html
433
413
  const originalEscape = Mustache.escape;
434
414
  let renderedHtml;
435
415
  try {
@@ -442,34 +422,45 @@ async function assembleSnippet({ snippetPath, templatePath, config, cwd, outputB
442
422
  }
443
423
  fs.writeFileSync(outPaths.normal, renderedHtml, 'utf8');
444
424
 
445
- // 3. 收集模板目录的额外 CSS + Juice CSS 内联 → .output.html
425
+ // 3. Juice CSS 内联 → .output.html
446
426
  const templateDir = path.dirname(templatePath);
447
427
  const extraCss = collectExtraCss(templateDir, config);
448
428
  const juiceOpts = Object.assign({}, config.juice || {});
449
429
  delete juiceOpts.extraCssFiles;
450
- const processed = juice(renderedHtml, { ...juiceOpts, extraCss });
430
+ let processed;
431
+ try {
432
+ processed = juice(renderedHtml, { ...juiceOpts, extraCss });
433
+ } catch (err) {
434
+ throw new Error(`CSS 内联失败:${err.message}`);
435
+ }
451
436
  fs.writeFileSync(outPaths.output, processed, 'utf8');
452
437
 
453
438
  // 4. 压缩 → .minified.html
454
- const minified = await minifyHtml(processed, config.minify);
439
+ let minified;
440
+ try {
441
+ minified = await minifyHtml(processed, config.minify);
442
+ } catch (err) {
443
+ throw new Error(`压缩失败:${err.message}`);
444
+ }
455
445
  fs.writeFileSync(outPaths.minified, minified, 'utf8');
456
446
 
457
447
  // 5. 报告
448
+ const rel = (p) => './' + path.relative(cwd, p);
458
449
  const layers = config._layers || [];
459
- const layerLines = layers
460
- .map((l) => ` ${chalk.gray('·')} ${l.label}`)
461
- .join('\n');
450
+ const layerLines = layers.length > 0
451
+ ? layers.map((l) => ` ${chalk.gray('·')} ${l.label}`).join('\n') + '\n'
452
+ : '';
462
453
 
463
454
  console.log(
464
455
  chalk.green('\n✔ 片段组装完成') + '\n' +
465
456
  ` ${chalk.bold('片段:')} ${chalk.cyan(snippetPath)}\n` +
466
457
  ` ${chalk.bold('模板:')} ${chalk.cyan(templatePath)}\n` +
467
- ` ${chalk.bold('配置层(低→高):')}\n${layerLines}\n` +
458
+ ` ${chalk.bold('配置层(低→高):')}\n${layerLines}` +
468
459
  ` ${chalk.bold('输出:')}\n` +
469
- ` ${chalk.green('·')} 原始组装 ${chalk.cyan(outPaths.raw)} ${chalk.gray('(' + fmtSize(rawMarkup) + ')')}\n` +
470
- ` ${chalk.green('·')} 已渲染 ${chalk.cyan(outPaths.normal)} ${chalk.gray('(' + fmtSize(renderedHtml) + ')')}\n` +
471
- ` ${chalk.green('·')} 内联后 ${chalk.cyan(outPaths.output)} ${chalk.gray('(' + fmtSize(processed) + ')')}\n` +
472
- ` ${chalk.green('·')} 压缩版 ${chalk.cyan(outPaths.minified)} ${chalk.gray('(' + fmtSize(minified) + ',节省 ' + savings(processed, minified) + ')')}`
460
+ ` ${chalk.green('·')} 原始组装 ${chalk.cyan(rel(outPaths.raw))} ${chalk.gray(fmtSize(rawMarkup))}\n` +
461
+ ` ${chalk.green('·')} 已渲染 ${chalk.cyan(rel(outPaths.normal))} ${chalk.gray(fmtSize(renderedHtml))}\n` +
462
+ ` ${chalk.green('·')} 内联后 ${chalk.cyan(rel(outPaths.output))} ${chalk.gray(fmtSize(processed))}\n` +
463
+ ` ${chalk.green('·')} 压缩版 ${chalk.cyan(rel(outPaths.minified))} ${chalk.gray(fmtSize(minified) + ',节省 ' + savings(processed, minified))}`
473
464
  );
474
465
 
475
466
  return outPaths;
@@ -715,7 +706,7 @@ async function promptConfirm(summary) {
715
706
  console.log(` 片段变体: ${chalk.green(summary.variantName)} ${chalk.gray(`(${summary.variant})`)}`);
716
707
  console.log(` 配置 YAML: ${chalk.green(summary.configFile)}`);
717
708
  console.log(chalk.gray('───────────────────────────────────────────'));
718
- console.log(` 输出目录: ${chalk.cyan(summary.outputDir)}`);
709
+ console.log(` 输出目录: ${chalk.cyan('./')}`);
719
710
  console.log(` 输出文件名: ${chalk.green(summary.outputBaseName)}`);
720
711
  console.log(` 输出文件:`);
721
712
  console.log(` ${chalk.green('·')} ${path.basename(outPaths.raw)} ${chalk.gray('(未渲染,无 CSS 内联)')}`);
@@ -955,7 +946,7 @@ async function runInteractiveMode({ config: cliConfigPath }) {
955
946
  });
956
947
  }
957
948
 
958
- module.exports = {
949
+ export {
959
950
  runSnippetMode,
960
951
  runInteractiveMode,
961
952
  // EDM resolution & scanning
@@ -969,7 +960,6 @@ module.exports = {
969
960
  findHtmlFiles,
970
961
  findYamlFiles,
971
962
  findLocalConfig,
972
- collectConfigs,
973
963
  getBrand,
974
964
  filterSeries,
975
965
  // config
package/src/view.js CHANGED
@@ -1,12 +1,10 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const chalk = require('chalk');
6
- const {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import {
7
5
  resolveEdmDir, loadMeta, findBrands, findTemplateVersions,
8
6
  findSeriesDirs, findSnippetVariants, findConfigs,
9
- } = require('./snippet');
7
+ } from './snippet.js';
10
8
 
11
9
  function fmtBytes(b) {
12
10
  return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
@@ -349,7 +347,7 @@ async function showCheckbox(title, choices) {
349
347
  */
350
348
  async function copyResource(type, resourcePath, cwd) {
351
349
  try {
352
- const { runInitMode } = require('./init');
350
+ const { runInitMode } = await import('./init.js');
353
351
  if (type === 'template') {
354
352
  await runInitMode({ template: resourcePath });
355
353
  } else if (type === 'snippet') {
@@ -616,7 +614,7 @@ async function interactiveBrowse(edmDir, startParsed) {
616
614
  // Include template version in default name for clarity
617
615
  const versionName = versions.length > 0 ? versions[0].name : 'template';
618
616
  const defaultBaseName = `${current.brand}-${versionName}-${current.series}-${current.variant}`;
619
- const { promptOutputName: pn } = require('./snippet');
617
+ const { promptOutputName: pn } = await import('./snippet.js');
620
618
  const outputBaseName = await pn(defaultBaseName, process.cwd());
621
619
  const cwd = process.cwd();
622
620
  const cwdRel = (p) => './' + path.relative(cwd, p);
@@ -714,7 +712,7 @@ async function runViewMode({ viewPath, interactive, scope }) {
714
712
  });
715
713
  if (action === 'copy') {
716
714
  console.log();
717
- const { runInitMode } = require('./init');
715
+ const { runInitMode } = await import('./init.js');
718
716
  await runInitMode({ template: picked.version.templatePath });
719
717
  console.log();
720
718
  }
@@ -792,7 +790,7 @@ async function runViewMode({ viewPath, interactive, scope }) {
792
790
  actions
793
791
  );
794
792
  if (action === 'exit') return;
795
- const { runInitMode } = require('./init');
793
+ const { runInitMode } = await import('./init.js');
796
794
  console.log();
797
795
  if (action === 'snippet') {
798
796
  await runInitMode({ snippet: snipPath });
@@ -872,7 +870,7 @@ async function runViewMode({ viewPath, interactive, scope }) {
872
870
  variantData: picked.variant,
873
871
  };
874
872
  } else {
875
- const { runInitMode } = require('./init');
873
+ const { runInitMode } = await import('./init.js');
876
874
  console.log();
877
875
  if (action === 'copy-snippet') {
878
876
  await runInitMode({ snippet: picked.snippetPath });
@@ -938,4 +936,4 @@ async function runViewMode({ viewPath, interactive, scope }) {
938
936
  printFullTree(edmDir);
939
937
  }
940
938
 
941
- module.exports = { runViewMode, parseViewPath };
939
+ export { runViewMode, parseViewPath };