@emailens/cli 0.1.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/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +1 -0
- package/dist/maizzle-JGKTYDDO.js +55 -0
- package/dist/maizzle-JGKTYDDO.js.map +1 -0
- package/dist/mjml-KG66APFB.js +48 -0
- package/dist/mjml-KG66APFB.js.map +1 -0
- package/dist/react-email-ZWLGXROP.js +164 -0
- package/dist/react-email-ZWLGXROP.js.map +1 -0
- package/dist/screenshots-PMZQU5V7.js +62 -0
- package/dist/screenshots-PMZQU5V7.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand as defineCommand5, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/commands/analyze.ts
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import {
|
|
10
|
+
analyzeEmail,
|
|
11
|
+
generateCompatibilityScore
|
|
12
|
+
} from "@emailens/engine";
|
|
13
|
+
|
|
14
|
+
// src/utils.ts
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { EMAIL_CLIENTS } from "@emailens/engine";
|
|
17
|
+
|
|
18
|
+
// src/compile/detect.ts
|
|
19
|
+
import { extname } from "path";
|
|
20
|
+
function detectFormat(filePath) {
|
|
21
|
+
const ext = extname(filePath).toLowerCase();
|
|
22
|
+
switch (ext) {
|
|
23
|
+
case ".tsx":
|
|
24
|
+
case ".jsx":
|
|
25
|
+
return "jsx";
|
|
26
|
+
case ".mjml":
|
|
27
|
+
return "mjml";
|
|
28
|
+
case ".html":
|
|
29
|
+
case ".htm":
|
|
30
|
+
return "html";
|
|
31
|
+
default:
|
|
32
|
+
return "html";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/utils.ts
|
|
37
|
+
var VALID_FORMATS = /* @__PURE__ */ new Set(["html", "jsx", "mjml", "maizzle"]);
|
|
38
|
+
async function readInput(pathOrStdin) {
|
|
39
|
+
if (pathOrStdin === "-") {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync("/dev/stdin", "utf-8");
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve2, reject) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
process.stdin.resume();
|
|
47
|
+
process.stdin.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
48
|
+
process.stdin.on("end", () => {
|
|
49
|
+
const data = Buffer.concat(chunks).toString("utf-8");
|
|
50
|
+
if (!data.trim()) {
|
|
51
|
+
reject(new Error("No input received from stdin. Pipe HTML or provide a file path."));
|
|
52
|
+
} else {
|
|
53
|
+
resolve2(data);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
process.stdin.on("error", () => {
|
|
57
|
+
reject(new Error("No input received from stdin. Pipe HTML or provide a file path."));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(pathOrStdin, "utf-8");
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
throw new Error(`Failed to read file "${pathOrStdin}": ${message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function resolveClients(ids) {
|
|
69
|
+
const validIds = new Set(EMAIL_CLIENTS.map((c) => c.id));
|
|
70
|
+
if (!ids) {
|
|
71
|
+
return EMAIL_CLIENTS.map((c) => c.id);
|
|
72
|
+
}
|
|
73
|
+
const requested = ids.split(",").map((s) => s.trim()).filter(Boolean);
|
|
74
|
+
const invalid = requested.filter((id) => !validIds.has(id));
|
|
75
|
+
if (invalid.length > 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Unknown client ID(s): ${invalid.join(", ")}.
|
|
78
|
+
Valid IDs: ${Array.from(validIds).join(", ")}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return requested;
|
|
82
|
+
}
|
|
83
|
+
function resolveFormat(formatFlag, filePath) {
|
|
84
|
+
if (formatFlag) {
|
|
85
|
+
if (!VALID_FORMATS.has(formatFlag)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Unknown format "${formatFlag}". Valid formats: ${Array.from(VALID_FORMATS).join(", ")}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return formatFlag;
|
|
91
|
+
}
|
|
92
|
+
if (filePath && filePath !== "-") {
|
|
93
|
+
return detectFormat(filePath);
|
|
94
|
+
}
|
|
95
|
+
return "html";
|
|
96
|
+
}
|
|
97
|
+
function toFramework(format) {
|
|
98
|
+
if (format === "jsx" || format === "mjml" || format === "maizzle") return format;
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/compile/index.ts
|
|
103
|
+
async function compile(source, format, _filePath) {
|
|
104
|
+
switch (format) {
|
|
105
|
+
case "html":
|
|
106
|
+
return source;
|
|
107
|
+
case "jsx": {
|
|
108
|
+
const { compileReactEmail } = await import("./react-email-ZWLGXROP.js");
|
|
109
|
+
return compileReactEmail(source);
|
|
110
|
+
}
|
|
111
|
+
case "mjml": {
|
|
112
|
+
const { compileMjml } = await import("./mjml-KG66APFB.js");
|
|
113
|
+
return compileMjml(source);
|
|
114
|
+
}
|
|
115
|
+
case "maizzle": {
|
|
116
|
+
const { compileMaizzle } = await import("./maizzle-JGKTYDDO.js");
|
|
117
|
+
return compileMaizzle(source);
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`Unknown format: "${format}". Use html, jsx, mjml, or maizzle.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/output/terminal.ts
|
|
125
|
+
import pc from "picocolors";
|
|
126
|
+
import Table from "cli-table3";
|
|
127
|
+
import {
|
|
128
|
+
EMAIL_CLIENTS as EMAIL_CLIENTS2
|
|
129
|
+
} from "@emailens/engine";
|
|
130
|
+
function colorScore(score) {
|
|
131
|
+
const str = String(score);
|
|
132
|
+
if (score >= 90) return pc.green(str);
|
|
133
|
+
if (score >= 70) return pc.yellow(str);
|
|
134
|
+
return pc.red(str);
|
|
135
|
+
}
|
|
136
|
+
function severityIcon(severity) {
|
|
137
|
+
switch (severity) {
|
|
138
|
+
case "error":
|
|
139
|
+
return pc.red("\u2717");
|
|
140
|
+
case "warning":
|
|
141
|
+
return pc.yellow("\u26A0");
|
|
142
|
+
case "info":
|
|
143
|
+
return pc.blue("\u2139");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function worstSeverity(warnings) {
|
|
147
|
+
if (warnings.some((w) => w.severity === "error")) return "error";
|
|
148
|
+
if (warnings.some((w) => w.severity === "warning")) return "warning";
|
|
149
|
+
return "info";
|
|
150
|
+
}
|
|
151
|
+
function formatIssueCount(warnings) {
|
|
152
|
+
const errors = warnings.filter((w) => w.severity === "error").length;
|
|
153
|
+
const warns = warnings.filter((w) => w.severity === "warning").length;
|
|
154
|
+
const infos = warnings.filter((w) => w.severity === "info").length;
|
|
155
|
+
const parts = [];
|
|
156
|
+
if (errors > 0) parts.push(pc.red(`${errors} err`));
|
|
157
|
+
if (warns > 0) parts.push(pc.yellow(`${warns} warn`));
|
|
158
|
+
if (infos > 0) parts.push(pc.blue(`${infos} info`));
|
|
159
|
+
return parts.join(", ") || pc.green("none");
|
|
160
|
+
}
|
|
161
|
+
function printScoreTable(scores, warnings, options) {
|
|
162
|
+
if (!options?.quiet) {
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(pc.bold(" Emailens \u2014 Compatibility Report"));
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
const clientMap = new Map(EMAIL_CLIENTS2.map((c) => [c.id, c.name]));
|
|
168
|
+
const warningsByClient = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const w of warnings) {
|
|
170
|
+
const existing = warningsByClient.get(w.client) ?? [];
|
|
171
|
+
existing.push(w);
|
|
172
|
+
warningsByClient.set(w.client, existing);
|
|
173
|
+
}
|
|
174
|
+
const table = new Table({
|
|
175
|
+
head: [pc.bold("Client"), pc.bold("Score"), pc.bold("Issues")],
|
|
176
|
+
style: { head: [], border: [] }
|
|
177
|
+
});
|
|
178
|
+
const sortedClients = Object.entries(scores).sort(([, a], [, b]) => a.score - b.score);
|
|
179
|
+
for (const [clientId, { score }] of sortedClients) {
|
|
180
|
+
const clientWarnings = warningsByClient.get(clientId) ?? [];
|
|
181
|
+
table.push([
|
|
182
|
+
clientMap.get(clientId) ?? clientId,
|
|
183
|
+
colorScore(score),
|
|
184
|
+
formatIssueCount(clientWarnings)
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
console.log(table.toString());
|
|
188
|
+
const scoreValues = Object.values(scores);
|
|
189
|
+
const overall = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(` Overall: ${colorScore(overall)}/100`);
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
function printWarnings(warnings, options) {
|
|
195
|
+
if (warnings.length === 0) {
|
|
196
|
+
if (!options?.quiet) {
|
|
197
|
+
console.log(pc.green(" No compatibility issues found."));
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const byClient = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const w of warnings) {
|
|
204
|
+
const existing = byClient.get(w.client) ?? [];
|
|
205
|
+
existing.push(w);
|
|
206
|
+
byClient.set(w.client, existing);
|
|
207
|
+
}
|
|
208
|
+
const clientMap = new Map(EMAIL_CLIENTS2.map((c) => [c.id, c.name]));
|
|
209
|
+
for (const [clientId, clientWarnings] of byClient) {
|
|
210
|
+
const clientName = clientMap.get(clientId) ?? clientId;
|
|
211
|
+
const worst = worstSeverity(clientWarnings);
|
|
212
|
+
console.log(` ${severityIcon(worst)} ${pc.bold(clientName)} (${clientWarnings.length} issue${clientWarnings.length === 1 ? "" : "s"})`);
|
|
213
|
+
for (const w of clientWarnings) {
|
|
214
|
+
const prop = pc.dim(w.property);
|
|
215
|
+
console.log(` ${severityIcon(w.severity)} ${prop} \u2014 ${w.message}`);
|
|
216
|
+
if (w.suggestion) {
|
|
217
|
+
console.log(` ${pc.dim("\u2192")} ${w.suggestion}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
console.log();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/output/json.ts
|
|
225
|
+
function formatJsonOutput(opts) {
|
|
226
|
+
const scoreValues = Object.values(opts.scores);
|
|
227
|
+
const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
|
|
228
|
+
const output = {
|
|
229
|
+
overallScore,
|
|
230
|
+
scores: opts.scores,
|
|
231
|
+
warnings: opts.warnings.map((w) => ({
|
|
232
|
+
client: w.client,
|
|
233
|
+
property: w.property,
|
|
234
|
+
severity: w.severity,
|
|
235
|
+
message: w.message,
|
|
236
|
+
suggestion: w.suggestion,
|
|
237
|
+
fix: w.fix ? {
|
|
238
|
+
before: w.fix.before,
|
|
239
|
+
after: w.fix.after,
|
|
240
|
+
language: w.fix.language,
|
|
241
|
+
description: w.fix.description
|
|
242
|
+
} : void 0
|
|
243
|
+
}))
|
|
244
|
+
};
|
|
245
|
+
if (opts.transforms) {
|
|
246
|
+
output.transforms = opts.transforms.map((t) => ({
|
|
247
|
+
clientId: t.clientId,
|
|
248
|
+
html: t.html
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
if (opts.darkMode) {
|
|
252
|
+
output.darkMode = {};
|
|
253
|
+
for (const [clientId, dm] of Object.entries(opts.darkMode)) {
|
|
254
|
+
output.darkMode[clientId] = {
|
|
255
|
+
html: dm.html,
|
|
256
|
+
warningCount: dm.warnings.length
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return output;
|
|
261
|
+
}
|
|
262
|
+
function printJson(opts) {
|
|
263
|
+
console.log(JSON.stringify(formatJsonOutput(opts), null, 2));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/commands/analyze.ts
|
|
267
|
+
var analyze_default = defineCommand({
|
|
268
|
+
meta: {
|
|
269
|
+
name: "analyze",
|
|
270
|
+
description: "Analyze email CSS compatibility across clients"
|
|
271
|
+
},
|
|
272
|
+
args: {
|
|
273
|
+
input: {
|
|
274
|
+
type: "positional",
|
|
275
|
+
description: "HTML file path or - for stdin",
|
|
276
|
+
required: true
|
|
277
|
+
},
|
|
278
|
+
format: {
|
|
279
|
+
type: "string",
|
|
280
|
+
alias: "f",
|
|
281
|
+
description: "Input format: html, jsx, mjml, maizzle"
|
|
282
|
+
},
|
|
283
|
+
clients: {
|
|
284
|
+
type: "string",
|
|
285
|
+
alias: "c",
|
|
286
|
+
description: "Comma-separated client IDs to filter"
|
|
287
|
+
},
|
|
288
|
+
json: {
|
|
289
|
+
type: "boolean",
|
|
290
|
+
description: "Output as JSON"
|
|
291
|
+
},
|
|
292
|
+
quiet: {
|
|
293
|
+
type: "boolean",
|
|
294
|
+
alias: "q",
|
|
295
|
+
description: "Suppress spinners and decorations"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
async run({ args }) {
|
|
299
|
+
const spinner = args.quiet || args.json ? null : ora();
|
|
300
|
+
try {
|
|
301
|
+
const format = resolveFormat(args.format, args.input);
|
|
302
|
+
spinner?.start("Reading input...");
|
|
303
|
+
const source = await readInput(args.input);
|
|
304
|
+
spinner?.succeed("Input read");
|
|
305
|
+
const html = await compile(source, format, args.input !== "-" ? args.input : void 0);
|
|
306
|
+
const clientIds = resolveClients(args.clients);
|
|
307
|
+
spinner?.start("Analyzing compatibility...");
|
|
308
|
+
const framework = toFramework(format);
|
|
309
|
+
const warnings = analyzeEmail(html, framework);
|
|
310
|
+
const allScores = generateCompatibilityScore(warnings);
|
|
311
|
+
const clientSet = new Set(clientIds);
|
|
312
|
+
const scores = {};
|
|
313
|
+
for (const [id, data] of Object.entries(allScores)) {
|
|
314
|
+
if (clientSet.has(id)) {
|
|
315
|
+
scores[id] = data;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));
|
|
319
|
+
spinner?.succeed("Analysis complete");
|
|
320
|
+
if (args.json) {
|
|
321
|
+
printJson({ scores, warnings: filteredWarnings });
|
|
322
|
+
} else {
|
|
323
|
+
printScoreTable(scores, filteredWarnings, { quiet: args.quiet });
|
|
324
|
+
printWarnings(filteredWarnings, { quiet: args.quiet });
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
spinner?.fail(err.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// src/commands/preview.ts
|
|
334
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
335
|
+
import ora2 from "ora";
|
|
336
|
+
import {
|
|
337
|
+
transformForAllClients,
|
|
338
|
+
analyzeEmail as analyzeEmail2,
|
|
339
|
+
generateCompatibilityScore as generateCompatibilityScore2,
|
|
340
|
+
simulateDarkMode
|
|
341
|
+
} from "@emailens/engine";
|
|
342
|
+
var preview_default = defineCommand2({
|
|
343
|
+
meta: {
|
|
344
|
+
name: "preview",
|
|
345
|
+
description: "Full email preview: transforms, analysis, dark mode, screenshots"
|
|
346
|
+
},
|
|
347
|
+
args: {
|
|
348
|
+
input: {
|
|
349
|
+
type: "positional",
|
|
350
|
+
description: "HTML file path or - for stdin",
|
|
351
|
+
required: true
|
|
352
|
+
},
|
|
353
|
+
format: {
|
|
354
|
+
type: "string",
|
|
355
|
+
alias: "f",
|
|
356
|
+
description: "Input format: html, jsx, mjml, maizzle"
|
|
357
|
+
},
|
|
358
|
+
clients: {
|
|
359
|
+
type: "string",
|
|
360
|
+
alias: "c",
|
|
361
|
+
description: "Comma-separated client IDs to filter"
|
|
362
|
+
},
|
|
363
|
+
"dark-mode": {
|
|
364
|
+
type: "boolean",
|
|
365
|
+
alias: "d",
|
|
366
|
+
description: "Include dark mode simulation"
|
|
367
|
+
},
|
|
368
|
+
screenshots: {
|
|
369
|
+
type: "boolean",
|
|
370
|
+
description: "Capture screenshots (requires BROWSERLESS_URL)"
|
|
371
|
+
},
|
|
372
|
+
out: {
|
|
373
|
+
type: "string",
|
|
374
|
+
alias: "o",
|
|
375
|
+
description: "Output directory for screenshots"
|
|
376
|
+
},
|
|
377
|
+
json: {
|
|
378
|
+
type: "boolean",
|
|
379
|
+
description: "Output as JSON"
|
|
380
|
+
},
|
|
381
|
+
quiet: {
|
|
382
|
+
type: "boolean",
|
|
383
|
+
alias: "q",
|
|
384
|
+
description: "Suppress spinners and decorations"
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
async run({ args }) {
|
|
388
|
+
const spinner = args.quiet || args.json ? null : ora2();
|
|
389
|
+
try {
|
|
390
|
+
const format = resolveFormat(args.format, args.input);
|
|
391
|
+
spinner?.start("Reading input...");
|
|
392
|
+
const source = await readInput(args.input);
|
|
393
|
+
spinner?.succeed("Input read");
|
|
394
|
+
const html = await compile(source, format, args.input !== "-" ? args.input : void 0);
|
|
395
|
+
const clientIds = resolveClients(args.clients);
|
|
396
|
+
const clientSet = new Set(clientIds);
|
|
397
|
+
spinner?.start("Transforming for all clients...");
|
|
398
|
+
const allTransforms = transformForAllClients(html);
|
|
399
|
+
const transforms = allTransforms.filter((t) => clientSet.has(t.clientId));
|
|
400
|
+
spinner?.succeed(`Transformed for ${transforms.length} clients`);
|
|
401
|
+
spinner?.start("Analyzing compatibility...");
|
|
402
|
+
const framework = toFramework(format);
|
|
403
|
+
const warnings = analyzeEmail2(html, framework);
|
|
404
|
+
const allScores = generateCompatibilityScore2(warnings);
|
|
405
|
+
const scores = {};
|
|
406
|
+
for (const [id, data] of Object.entries(allScores)) {
|
|
407
|
+
if (clientSet.has(id)) {
|
|
408
|
+
scores[id] = data;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));
|
|
412
|
+
spinner?.succeed("Analysis complete");
|
|
413
|
+
let darkMode;
|
|
414
|
+
if (args["dark-mode"]) {
|
|
415
|
+
spinner?.start("Simulating dark mode...");
|
|
416
|
+
darkMode = {};
|
|
417
|
+
for (const t of transforms) {
|
|
418
|
+
darkMode[t.clientId] = simulateDarkMode(t.html, t.clientId);
|
|
419
|
+
}
|
|
420
|
+
spinner?.succeed("Dark mode simulation complete");
|
|
421
|
+
}
|
|
422
|
+
if (args.screenshots) {
|
|
423
|
+
const outDir = args.out ?? "./emailens-screenshots";
|
|
424
|
+
spinner?.start("Capturing screenshots...");
|
|
425
|
+
try {
|
|
426
|
+
const { captureScreenshots } = await import("./screenshots-PMZQU5V7.js");
|
|
427
|
+
const results = await captureScreenshots(
|
|
428
|
+
transforms,
|
|
429
|
+
outDir,
|
|
430
|
+
(clientId, index, total) => {
|
|
431
|
+
if (spinner) spinner.text = `Capturing screenshots (${index}/${total}: ${clientId})...`;
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
spinner?.succeed(`Screenshots saved to ${outDir} (${results.size} files)`);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
spinner?.warn(`Screenshots skipped: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (args.json) {
|
|
440
|
+
printJson({ scores, warnings: filteredWarnings, transforms, darkMode });
|
|
441
|
+
} else {
|
|
442
|
+
printScoreTable(scores, filteredWarnings, { quiet: args.quiet });
|
|
443
|
+
printWarnings(filteredWarnings, { quiet: args.quiet });
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
spinner?.fail(err.message);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// src/commands/clients.ts
|
|
453
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
454
|
+
import pc2 from "picocolors";
|
|
455
|
+
import Table2 from "cli-table3";
|
|
456
|
+
import { EMAIL_CLIENTS as EMAIL_CLIENTS3 } from "@emailens/engine";
|
|
457
|
+
var clients_default = defineCommand3({
|
|
458
|
+
meta: {
|
|
459
|
+
name: "clients",
|
|
460
|
+
description: "List all supported email clients"
|
|
461
|
+
},
|
|
462
|
+
args: {
|
|
463
|
+
json: {
|
|
464
|
+
type: "boolean",
|
|
465
|
+
description: "Output as JSON"
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
run({ args }) {
|
|
469
|
+
if (args.json) {
|
|
470
|
+
const data = EMAIL_CLIENTS3.map((c) => ({
|
|
471
|
+
id: c.id,
|
|
472
|
+
name: c.name,
|
|
473
|
+
category: c.category,
|
|
474
|
+
engine: c.engine,
|
|
475
|
+
darkModeSupport: c.darkModeSupport
|
|
476
|
+
}));
|
|
477
|
+
console.log(JSON.stringify(data, null, 2));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.log();
|
|
481
|
+
console.log(pc2.bold(" Emailens \u2014 Supported Email Clients"));
|
|
482
|
+
console.log();
|
|
483
|
+
const table = new Table2({
|
|
484
|
+
head: [
|
|
485
|
+
pc2.bold("ID"),
|
|
486
|
+
pc2.bold("Name"),
|
|
487
|
+
pc2.bold("Category"),
|
|
488
|
+
pc2.bold("Engine"),
|
|
489
|
+
pc2.bold("Dark Mode")
|
|
490
|
+
],
|
|
491
|
+
style: { head: [], border: [] }
|
|
492
|
+
});
|
|
493
|
+
for (const client of EMAIL_CLIENTS3) {
|
|
494
|
+
table.push([
|
|
495
|
+
client.id,
|
|
496
|
+
client.name,
|
|
497
|
+
client.category,
|
|
498
|
+
client.engine,
|
|
499
|
+
client.darkModeSupport ? pc2.green("Yes") : pc2.dim("No")
|
|
500
|
+
]);
|
|
501
|
+
}
|
|
502
|
+
console.log(table.toString());
|
|
503
|
+
console.log();
|
|
504
|
+
console.log(pc2.dim(` ${EMAIL_CLIENTS3.length} clients supported`));
|
|
505
|
+
console.log();
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// src/commands/export.ts
|
|
510
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
511
|
+
import { join, resolve } from "path";
|
|
512
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
513
|
+
import ora3 from "ora";
|
|
514
|
+
import {
|
|
515
|
+
transformForAllClients as transformForAllClients2,
|
|
516
|
+
analyzeEmail as analyzeEmail3,
|
|
517
|
+
generateCompatibilityScore as generateCompatibilityScore3,
|
|
518
|
+
simulateDarkMode as simulateDarkMode2
|
|
519
|
+
} from "@emailens/engine";
|
|
520
|
+
|
|
521
|
+
// src/output/html-report.ts
|
|
522
|
+
import { EMAIL_CLIENTS as EMAIL_CLIENTS4 } from "@emailens/engine";
|
|
523
|
+
function generateHtmlReport(opts) {
|
|
524
|
+
const clientMap = new Map(EMAIL_CLIENTS4.map((c) => [c.id, c]));
|
|
525
|
+
const scoreValues = Object.values(opts.scores);
|
|
526
|
+
const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
|
|
527
|
+
const scoreColor = (s) => s >= 90 ? "#22c55e" : s >= 70 ? "#eab308" : "#ef4444";
|
|
528
|
+
const clientCards = Object.entries(opts.scores).sort(([, a], [, b]) => a.score - b.score).map(([clientId, { score, errors, warnings: warns, info }]) => {
|
|
529
|
+
const client = clientMap.get(clientId);
|
|
530
|
+
const name = client?.name ?? clientId;
|
|
531
|
+
const clientWarnings = opts.warnings.filter((w) => w.client === clientId);
|
|
532
|
+
const issueCount = errors + warns + info;
|
|
533
|
+
const warningRows = clientWarnings.map((w) => {
|
|
534
|
+
const icon = w.severity === "error" ? "✗" : w.severity === "warning" ? "⚠" : "ℹ";
|
|
535
|
+
const color = w.severity === "error" ? "#ef4444" : w.severity === "warning" ? "#eab308" : "#3b82f6";
|
|
536
|
+
return `<div style="padding:4px 0;border-bottom:1px solid #f1f1f1">
|
|
537
|
+
<span style="color:${color}">${icon}</span>
|
|
538
|
+
<strong>${escapeHtml(w.property)}</strong> \u2014 ${escapeHtml(w.message)}
|
|
539
|
+
${w.suggestion ? `<br><span style="color:#888;margin-left:20px">→ ${escapeHtml(w.suggestion)}</span>` : ""}
|
|
540
|
+
</div>`;
|
|
541
|
+
}).join("");
|
|
542
|
+
return `<div style="border:1px solid #e5e5e5;border-radius:8px;padding:16px;margin-bottom:16px">
|
|
543
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
544
|
+
<h3 style="margin:0;font-size:16px">${escapeHtml(name)}</h3>
|
|
545
|
+
<span style="font-size:24px;font-weight:bold;color:${scoreColor(score)}">${score}</span>
|
|
546
|
+
</div>
|
|
547
|
+
<div style="font-size:13px;color:#888;margin-bottom:8px">${client?.engine ?? ""} · ${client?.category ?? ""} · ${issueCount} issue${issueCount === 1 ? "" : "s"}</div>
|
|
548
|
+
${clientWarnings.length > 0 ? `<div style="font-size:13px">${warningRows}</div>` : '<div style="color:#22c55e;font-size:13px">No issues</div>'}
|
|
549
|
+
</div>`;
|
|
550
|
+
}).join("");
|
|
551
|
+
return `<!DOCTYPE html>
|
|
552
|
+
<html lang="en">
|
|
553
|
+
<head>
|
|
554
|
+
<meta charset="utf-8">
|
|
555
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
556
|
+
<title>Emailens Compatibility Report</title>
|
|
557
|
+
<style>
|
|
558
|
+
* { box-sizing: border-box; }
|
|
559
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 24px; background: #fafafa; color: #333; }
|
|
560
|
+
.container { max-width: 900px; margin: 0 auto; }
|
|
561
|
+
.header { text-align: center; margin-bottom: 32px; }
|
|
562
|
+
.header h1 { margin: 0 0 8px; font-size: 24px; }
|
|
563
|
+
.overall { font-size: 48px; font-weight: bold; }
|
|
564
|
+
.subtitle { color: #888; font-size: 14px; }
|
|
565
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 16px; }
|
|
566
|
+
.footer { text-align: center; margin-top: 32px; color: #aaa; font-size: 12px; }
|
|
567
|
+
</style>
|
|
568
|
+
</head>
|
|
569
|
+
<body>
|
|
570
|
+
<div class="container">
|
|
571
|
+
<div class="header">
|
|
572
|
+
<h1>Emailens Compatibility Report</h1>
|
|
573
|
+
<div class="overall" style="color:${scoreColor(overallScore)}">${overallScore}/100</div>
|
|
574
|
+
<div class="subtitle">${Object.keys(opts.scores).length} clients analyzed · ${opts.warnings.length} total issues</div>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="grid">
|
|
577
|
+
${clientCards}
|
|
578
|
+
</div>
|
|
579
|
+
<div class="footer">
|
|
580
|
+
Generated by <a href="https://github.com/emailens/cli" style="color:#888">@emailens/cli</a>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</body>
|
|
584
|
+
</html>`;
|
|
585
|
+
}
|
|
586
|
+
function escapeHtml(str) {
|
|
587
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/commands/export.ts
|
|
591
|
+
var export_default = defineCommand4({
|
|
592
|
+
meta: {
|
|
593
|
+
name: "export",
|
|
594
|
+
description: "Export compatibility report as JSON or HTML"
|
|
595
|
+
},
|
|
596
|
+
args: {
|
|
597
|
+
input: {
|
|
598
|
+
type: "positional",
|
|
599
|
+
description: "HTML file path or - for stdin",
|
|
600
|
+
required: true
|
|
601
|
+
},
|
|
602
|
+
format: {
|
|
603
|
+
type: "string",
|
|
604
|
+
alias: "f",
|
|
605
|
+
description: "Input format: html, jsx, mjml, maizzle"
|
|
606
|
+
},
|
|
607
|
+
clients: {
|
|
608
|
+
type: "string",
|
|
609
|
+
alias: "c",
|
|
610
|
+
description: "Comma-separated client IDs to filter"
|
|
611
|
+
},
|
|
612
|
+
"dark-mode": {
|
|
613
|
+
type: "boolean",
|
|
614
|
+
alias: "d",
|
|
615
|
+
description: "Include dark mode simulation"
|
|
616
|
+
},
|
|
617
|
+
screenshots: {
|
|
618
|
+
type: "boolean",
|
|
619
|
+
description: "Capture screenshots (requires BROWSERLESS_URL)"
|
|
620
|
+
},
|
|
621
|
+
out: {
|
|
622
|
+
type: "string",
|
|
623
|
+
alias: "o",
|
|
624
|
+
description: "Output directory (default: ./emailens-report)"
|
|
625
|
+
},
|
|
626
|
+
json: {
|
|
627
|
+
type: "boolean",
|
|
628
|
+
description: "Export as JSON instead of HTML"
|
|
629
|
+
},
|
|
630
|
+
quiet: {
|
|
631
|
+
type: "boolean",
|
|
632
|
+
alias: "q",
|
|
633
|
+
description: "Suppress spinners and decorations"
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
async run({ args }) {
|
|
637
|
+
const spinner = args.quiet ? null : ora3();
|
|
638
|
+
const outDir = resolve(args.out ?? "./emailens-report");
|
|
639
|
+
try {
|
|
640
|
+
const format = resolveFormat(args.format, args.input);
|
|
641
|
+
spinner?.start("Reading input...");
|
|
642
|
+
const source = await readInput(args.input);
|
|
643
|
+
spinner?.succeed("Input read");
|
|
644
|
+
const html = await compile(source, format, args.input !== "-" ? args.input : void 0);
|
|
645
|
+
const clientIds = resolveClients(args.clients);
|
|
646
|
+
const clientSet = new Set(clientIds);
|
|
647
|
+
spinner?.start("Transforming for all clients...");
|
|
648
|
+
const allTransforms = transformForAllClients2(html);
|
|
649
|
+
const transforms = allTransforms.filter((t) => clientSet.has(t.clientId));
|
|
650
|
+
spinner?.succeed(`Transformed for ${transforms.length} clients`);
|
|
651
|
+
spinner?.start("Analyzing compatibility...");
|
|
652
|
+
const framework = toFramework(format);
|
|
653
|
+
const warnings = analyzeEmail3(html, framework);
|
|
654
|
+
const allScores = generateCompatibilityScore3(warnings);
|
|
655
|
+
const scores = {};
|
|
656
|
+
for (const [id, data] of Object.entries(allScores)) {
|
|
657
|
+
if (clientSet.has(id)) {
|
|
658
|
+
scores[id] = data;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));
|
|
662
|
+
spinner?.succeed("Analysis complete");
|
|
663
|
+
let darkMode;
|
|
664
|
+
if (args["dark-mode"]) {
|
|
665
|
+
spinner?.start("Simulating dark mode...");
|
|
666
|
+
darkMode = {};
|
|
667
|
+
for (const t of transforms) {
|
|
668
|
+
darkMode[t.clientId] = simulateDarkMode2(t.html, t.clientId);
|
|
669
|
+
}
|
|
670
|
+
spinner?.succeed("Dark mode simulation complete");
|
|
671
|
+
}
|
|
672
|
+
if (args.screenshots) {
|
|
673
|
+
spinner?.start("Capturing screenshots...");
|
|
674
|
+
try {
|
|
675
|
+
const { captureScreenshots } = await import("./screenshots-PMZQU5V7.js");
|
|
676
|
+
const screenshotDir = join(outDir, "screenshots");
|
|
677
|
+
const results = await captureScreenshots(
|
|
678
|
+
transforms,
|
|
679
|
+
screenshotDir,
|
|
680
|
+
(clientId, index, total) => {
|
|
681
|
+
if (spinner) spinner.text = `Capturing screenshots (${index}/${total}: ${clientId})...`;
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
spinner?.succeed(`Screenshots saved (${results.size} files)`);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
spinner?.warn(`Screenshots skipped: ${err.message}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
mkdirSync(outDir, { recursive: true });
|
|
690
|
+
if (args.json) {
|
|
691
|
+
spinner?.start("Exporting JSON...");
|
|
692
|
+
const jsonData = formatJsonOutput({ scores, warnings: filteredWarnings, transforms });
|
|
693
|
+
const jsonPath = join(outDir, "report.json");
|
|
694
|
+
writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
|
|
695
|
+
spinner?.succeed(`JSON report saved to ${jsonPath}`);
|
|
696
|
+
} else {
|
|
697
|
+
spinner?.start("Generating HTML report...");
|
|
698
|
+
const htmlReport = generateHtmlReport({
|
|
699
|
+
scores,
|
|
700
|
+
warnings: filteredWarnings,
|
|
701
|
+
transforms,
|
|
702
|
+
darkMode,
|
|
703
|
+
originalHtml: html
|
|
704
|
+
});
|
|
705
|
+
const htmlPath = join(outDir, "report.html");
|
|
706
|
+
writeFileSync(htmlPath, htmlReport);
|
|
707
|
+
spinner?.succeed(`HTML report saved to ${htmlPath}`);
|
|
708
|
+
}
|
|
709
|
+
if (!args.quiet) {
|
|
710
|
+
console.log();
|
|
711
|
+
console.log(` Output: ${outDir}`);
|
|
712
|
+
console.log();
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
spinner?.fail(err.message);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// src/index.ts
|
|
722
|
+
var main = defineCommand5({
|
|
723
|
+
meta: {
|
|
724
|
+
name: "emailens",
|
|
725
|
+
version: "0.1.0",
|
|
726
|
+
description: "Email compatibility analysis CLI \u2014 preview how HTML emails render across 12 email clients."
|
|
727
|
+
},
|
|
728
|
+
subCommands: {
|
|
729
|
+
analyze: analyze_default,
|
|
730
|
+
preview: preview_default,
|
|
731
|
+
clients: clients_default,
|
|
732
|
+
export: export_default
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
runMain(main);
|
|
736
|
+
//# sourceMappingURL=index.js.map
|