@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/analyze.ts","../src/utils.ts","../src/compile/detect.ts","../src/compile/index.ts","../src/output/terminal.ts","../src/output/json.ts","../src/commands/preview.ts","../src/commands/clients.ts","../src/commands/export.ts","../src/output/html-report.ts"],"sourcesContent":["import { defineCommand, runMain } from \"citty\";\r\nimport analyze from \"./commands/analyze.js\";\r\nimport preview from \"./commands/preview.js\";\r\nimport clients from \"./commands/clients.js\";\r\nimport exportCmd from \"./commands/export.js\";\r\n\r\nconst main = defineCommand({\r\n meta: {\r\n name: \"emailens\",\r\n version: \"0.1.0\",\r\n description: \"Email compatibility analysis CLI — preview how HTML emails render across 12 email clients.\",\r\n },\r\n subCommands: {\r\n analyze,\r\n preview,\r\n clients,\r\n export: exportCmd,\r\n },\r\n});\r\n\r\nrunMain(main);\r\n","import { defineCommand } from \"citty\";\r\nimport ora from \"ora\";\r\nimport {\r\n analyzeEmail,\r\n generateCompatibilityScore,\r\n} from \"@emailens/engine\";\r\nimport { readInput, resolveClients, resolveFormat, toFramework } from \"../utils.js\";\r\nimport { compile } from \"../compile/index.js\";\r\nimport { printScoreTable, printWarnings } from \"../output/terminal.js\";\r\nimport { printJson } from \"../output/json.js\";\r\n\r\nexport default defineCommand({\r\n meta: {\r\n name: \"analyze\",\r\n description: \"Analyze email CSS compatibility across clients\",\r\n },\r\n args: {\r\n input: {\r\n type: \"positional\",\r\n description: \"HTML file path or - for stdin\",\r\n required: true,\r\n },\r\n format: {\r\n type: \"string\",\r\n alias: \"f\",\r\n description: \"Input format: html, jsx, mjml, maizzle\",\r\n },\r\n clients: {\r\n type: \"string\",\r\n alias: \"c\",\r\n description: \"Comma-separated client IDs to filter\",\r\n },\r\n json: {\r\n type: \"boolean\",\r\n description: \"Output as JSON\",\r\n },\r\n quiet: {\r\n type: \"boolean\",\r\n alias: \"q\",\r\n description: \"Suppress spinners and decorations\",\r\n },\r\n },\r\n async run({ args }) {\r\n // --json implies quiet spinners\r\n const spinner = (args.quiet || args.json) ? null : ora();\r\n\r\n try {\r\n // Resolve format early so we fail fast on invalid --format\r\n const format = resolveFormat(args.format, args.input);\r\n\r\n // Read input\r\n spinner?.start(\"Reading input...\");\r\n const source = await readInput(args.input);\r\n spinner?.succeed(\"Input read\");\r\n\r\n // Compile (passthrough for html, auto-detects from extension)\r\n const html = await compile(source, format, args.input !== \"-\" ? args.input : undefined);\r\n\r\n // Resolve clients\r\n const clientIds = resolveClients(args.clients);\r\n\r\n // Analyze\r\n spinner?.start(\"Analyzing compatibility...\");\r\n const framework = toFramework(format);\r\n const warnings = analyzeEmail(html, framework);\r\n const allScores = generateCompatibilityScore(warnings);\r\n\r\n // Filter scores to requested clients\r\n const clientSet = new Set(clientIds);\r\n const scores: typeof allScores = {};\r\n for (const [id, data] of Object.entries(allScores)) {\r\n if (clientSet.has(id)) {\r\n scores[id] = data;\r\n }\r\n }\r\n\r\n // Filter warnings to requested clients\r\n const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));\r\n\r\n spinner?.succeed(\"Analysis complete\");\r\n\r\n // Output\r\n if (args.json) {\r\n printJson({ scores, warnings: filteredWarnings });\r\n } else {\r\n printScoreTable(scores, filteredWarnings, { quiet: args.quiet });\r\n printWarnings(filteredWarnings, { quiet: args.quiet });\r\n }\r\n } catch (err) {\r\n spinner?.fail((err as Error).message);\r\n process.exit(1);\r\n }\r\n },\r\n});\r\n","import { readFileSync } from \"node:fs\";\r\nimport { EMAIL_CLIENTS, type Framework, type InputFormat } from \"@emailens/engine\";\r\nimport { detectFormat } from \"./compile/detect.js\";\r\n\r\nconst VALID_FORMATS = new Set([\"html\", \"jsx\", \"mjml\", \"maizzle\"]);\r\n\r\n/**\r\n * Read HTML input from a file path or stdin (`-`).\r\n * Always returns a Promise for consistent caller ergonomics.\r\n */\r\nexport async function readInput(pathOrStdin: string): Promise<string> {\r\n if (pathOrStdin === \"-\") {\r\n // Try /dev/stdin first (works on Unix and Git Bash on Windows)\r\n try {\r\n return readFileSync(\"/dev/stdin\", \"utf-8\");\r\n } catch {\r\n // Fall through to async approach\r\n }\r\n\r\n // Async stdin reading for Windows compatibility\r\n return new Promise<string>((resolve, reject) => {\r\n const chunks: Buffer[] = [];\r\n process.stdin.resume();\r\n process.stdin.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\r\n process.stdin.on(\"end\", () => {\r\n const data = Buffer.concat(chunks).toString(\"utf-8\");\r\n if (!data.trim()) {\r\n reject(new Error(\"No input received from stdin. Pipe HTML or provide a file path.\"));\r\n } else {\r\n resolve(data);\r\n }\r\n });\r\n process.stdin.on(\"error\", () => {\r\n reject(new Error(\"No input received from stdin. Pipe HTML or provide a file path.\"));\r\n });\r\n });\r\n }\r\n\r\n try {\r\n return readFileSync(pathOrStdin, \"utf-8\");\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n throw new Error(`Failed to read file \"${pathOrStdin}\": ${message}`);\r\n }\r\n}\r\n\r\n/**\r\n * Validate and resolve client IDs. Returns all clients if none specified.\r\n */\r\nexport function resolveClients(ids?: string): string[] {\r\n const validIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\r\n\r\n if (!ids) {\r\n return EMAIL_CLIENTS.map((c) => c.id);\r\n }\r\n\r\n const requested = ids.split(\",\").map((s) => s.trim()).filter(Boolean);\r\n const invalid = requested.filter((id) => !validIds.has(id));\r\n\r\n if (invalid.length > 0) {\r\n throw new Error(\r\n `Unknown client ID(s): ${invalid.join(\", \")}.\\n` +\r\n `Valid IDs: ${Array.from(validIds).join(\", \")}`\r\n );\r\n }\r\n\r\n return requested;\r\n}\r\n\r\n/**\r\n * Resolve the input format from explicit flag, file extension, or default.\r\n * Validates the --format flag and errors on unknown values.\r\n * Auto-detects from file extension when no flag is given.\r\n */\r\nexport function resolveFormat(formatFlag?: string, filePath?: string): InputFormat {\r\n if (formatFlag) {\r\n if (!VALID_FORMATS.has(formatFlag)) {\r\n throw new Error(\r\n `Unknown format \"${formatFlag}\". Valid formats: ${Array.from(VALID_FORMATS).join(\", \")}`\r\n );\r\n }\r\n return formatFlag as InputFormat;\r\n }\r\n\r\n // Auto-detect from file extension\r\n if (filePath && filePath !== \"-\") {\r\n return detectFormat(filePath);\r\n }\r\n\r\n return \"html\";\r\n}\r\n\r\n/**\r\n * Convert a format string to the engine's Framework type.\r\n */\r\nexport function toFramework(format: InputFormat): Framework | undefined {\r\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\r\n return undefined;\r\n}\r\n","import { extname } from \"node:path\";\r\nimport type { InputFormat } from \"@emailens/engine\";\r\n\r\n/**\r\n * Auto-detect input format from file extension.\r\n */\r\nexport function detectFormat(filePath: string): InputFormat {\r\n const ext = extname(filePath).toLowerCase();\r\n\r\n switch (ext) {\r\n case \".tsx\":\r\n case \".jsx\":\r\n return \"jsx\";\r\n case \".mjml\":\r\n return \"mjml\";\r\n case \".html\":\r\n case \".htm\":\r\n return \"html\";\r\n default:\r\n // For unknown extensions, check if the content starts with <mjml\r\n // This is handled at compile time; default to html here\r\n return \"html\";\r\n }\r\n}\r\n","import type { InputFormat } from \"@emailens/engine\";\r\n\r\n/**\r\n * Compile source to HTML based on format.\r\n * Returns HTML passthrough if format is \"html\".\r\n * Lazily imports framework compilers to avoid loading unnecessary deps.\r\n */\r\nexport async function compile(source: string, format: InputFormat, _filePath?: string): Promise<string> {\r\n switch (format) {\r\n case \"html\":\r\n return source;\r\n\r\n case \"jsx\": {\r\n const { compileReactEmail } = await import(\"./react-email.js\");\r\n return compileReactEmail(source);\r\n }\r\n\r\n case \"mjml\": {\r\n const { compileMjml } = await import(\"./mjml.js\");\r\n return compileMjml(source);\r\n }\r\n\r\n case \"maizzle\": {\r\n const { compileMaizzle } = await import(\"./maizzle.js\");\r\n return compileMaizzle(source);\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown format: \"${format}\". Use html, jsx, mjml, or maizzle.`);\r\n }\r\n}\r\n","import pc from \"picocolors\";\r\nimport Table from \"cli-table3\";\r\nimport {\r\n EMAIL_CLIENTS,\r\n type CSSWarning,\r\n} from \"@emailens/engine\";\r\n\r\n/**\r\n * Color-code a score value: green >=90, yellow >=70, red <70.\r\n */\r\nfunction colorScore(score: number): string {\r\n const str = String(score);\r\n if (score >= 90) return pc.green(str);\r\n if (score >= 70) return pc.yellow(str);\r\n return pc.red(str);\r\n}\r\n\r\n/**\r\n * Get severity icon.\r\n */\r\nfunction severityIcon(severity: CSSWarning[\"severity\"]): string {\r\n switch (severity) {\r\n case \"error\": return pc.red(\"✗\");\r\n case \"warning\": return pc.yellow(\"⚠\");\r\n case \"info\": return pc.blue(\"ℹ\");\r\n }\r\n}\r\n\r\n/**\r\n * Get the worst severity from a list of warnings.\r\n */\r\nfunction worstSeverity(warnings: CSSWarning[]): CSSWarning[\"severity\"] {\r\n if (warnings.some((w) => w.severity === \"error\")) return \"error\";\r\n if (warnings.some((w) => w.severity === \"warning\")) return \"warning\";\r\n return \"info\";\r\n}\r\n\r\n/**\r\n * Format issue count with severity summary.\r\n */\r\nfunction formatIssueCount(warnings: CSSWarning[]): string {\r\n const errors = warnings.filter((w) => w.severity === \"error\").length;\r\n const warns = warnings.filter((w) => w.severity === \"warning\").length;\r\n const infos = warnings.filter((w) => w.severity === \"info\").length;\r\n\r\n const parts: string[] = [];\r\n if (errors > 0) parts.push(pc.red(`${errors} err`));\r\n if (warns > 0) parts.push(pc.yellow(`${warns} warn`));\r\n if (infos > 0) parts.push(pc.blue(`${infos} info`));\r\n\r\n return parts.join(\", \") || pc.green(\"none\");\r\n}\r\n\r\n/**\r\n * Print the compatibility score table to terminal.\r\n */\r\nexport function printScoreTable(\r\n scores: Record<string, { score: number; errors: number; warnings: number; info: number }>,\r\n warnings: CSSWarning[],\r\n options?: { quiet?: boolean }\r\n): void {\r\n if (!options?.quiet) {\r\n console.log();\r\n console.log(pc.bold(\" Emailens — Compatibility Report\"));\r\n console.log();\r\n }\r\n\r\n const clientMap = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n // Group warnings by client\r\n const warningsByClient = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n const existing = warningsByClient.get(w.client) ?? [];\r\n existing.push(w);\r\n warningsByClient.set(w.client, existing);\r\n }\r\n\r\n // Score table\r\n const table = new Table({\r\n head: [pc.bold(\"Client\"), pc.bold(\"Score\"), pc.bold(\"Issues\")],\r\n style: { head: [], border: [] },\r\n });\r\n\r\n const sortedClients = Object.entries(scores).sort(([, a], [, b]) => a.score - b.score);\r\n\r\n for (const [clientId, { score }] of sortedClients) {\r\n const clientWarnings = warningsByClient.get(clientId) ?? [];\r\n table.push([\r\n clientMap.get(clientId) ?? clientId,\r\n colorScore(score),\r\n formatIssueCount(clientWarnings),\r\n ]);\r\n }\r\n\r\n console.log(table.toString());\r\n\r\n // Overall score\r\n const scoreValues = Object.values(scores);\r\n const overall = scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n console.log();\r\n console.log(` Overall: ${colorScore(overall)}/100`);\r\n console.log();\r\n}\r\n\r\n/**\r\n * Print detailed warnings grouped by client.\r\n */\r\nexport function printWarnings(\r\n warnings: CSSWarning[],\r\n options?: { quiet?: boolean }\r\n): void {\r\n if (warnings.length === 0) {\r\n if (!options?.quiet) {\r\n console.log(pc.green(\" No compatibility issues found.\"));\r\n console.log();\r\n }\r\n return;\r\n }\r\n\r\n // Group by client\r\n const byClient = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n const existing = byClient.get(w.client) ?? [];\r\n existing.push(w);\r\n byClient.set(w.client, existing);\r\n }\r\n\r\n const clientMap = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n for (const [clientId, clientWarnings] of byClient) {\r\n const clientName = clientMap.get(clientId) ?? clientId;\r\n const worst = worstSeverity(clientWarnings);\r\n console.log(` ${severityIcon(worst)} ${pc.bold(clientName)} (${clientWarnings.length} issue${clientWarnings.length === 1 ? \"\" : \"s\"})`);\r\n\r\n for (const w of clientWarnings) {\r\n const prop = pc.dim(w.property);\r\n console.log(` ${severityIcon(w.severity)} ${prop} — ${w.message}`);\r\n if (w.suggestion) {\r\n console.log(` ${pc.dim(\"→\")} ${w.suggestion}`);\r\n }\r\n }\r\n console.log();\r\n }\r\n}\r\n","import type { CSSWarning, TransformResult } from \"@emailens/engine\";\r\n\r\ninterface JsonOutput {\r\n overallScore: number;\r\n scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;\r\n warnings: Array<{\r\n client: string;\r\n property: string;\r\n severity: string;\r\n message: string;\r\n suggestion?: string;\r\n fix?: {\r\n before: string;\r\n after: string;\r\n language: string;\r\n description: string;\r\n };\r\n }>;\r\n transforms?: Array<{\r\n clientId: string;\r\n html: string;\r\n }>;\r\n darkMode?: Record<string, {\r\n html: string;\r\n warningCount: number;\r\n }>;\r\n}\r\n\r\n/**\r\n * Build the JSON output object. Reusable by both printJson() and export command.\r\n */\r\nexport function formatJsonOutput(opts: {\r\n scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;\r\n warnings: CSSWarning[];\r\n transforms?: TransformResult[];\r\n darkMode?: Record<string, { html: string; warnings: CSSWarning[] }>;\r\n}): JsonOutput {\r\n const scoreValues = Object.values(opts.scores);\r\n const overallScore = scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n const output: JsonOutput = {\r\n overallScore,\r\n scores: opts.scores,\r\n warnings: opts.warnings.map((w) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix ? {\r\n before: w.fix.before,\r\n after: w.fix.after,\r\n language: w.fix.language,\r\n description: w.fix.description,\r\n } : undefined,\r\n })),\r\n };\r\n\r\n if (opts.transforms) {\r\n output.transforms = opts.transforms.map((t) => ({\r\n clientId: t.clientId,\r\n html: t.html,\r\n }));\r\n }\r\n\r\n if (opts.darkMode) {\r\n output.darkMode = {};\r\n for (const [clientId, dm] of Object.entries(opts.darkMode)) {\r\n output.darkMode[clientId] = {\r\n html: dm.html,\r\n warningCount: dm.warnings.length,\r\n };\r\n }\r\n }\r\n\r\n return output;\r\n}\r\n\r\n/**\r\n * Format analysis/preview results as JSON and print to stdout.\r\n */\r\nexport function printJson(opts: {\r\n scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;\r\n warnings: CSSWarning[];\r\n transforms?: TransformResult[];\r\n darkMode?: Record<string, { html: string; warnings: CSSWarning[] }>;\r\n}): void {\r\n console.log(JSON.stringify(formatJsonOutput(opts), null, 2));\r\n}\r\n","import { defineCommand } from \"citty\";\r\nimport ora from \"ora\";\r\nimport {\r\n transformForAllClients,\r\n analyzeEmail,\r\n generateCompatibilityScore,\r\n simulateDarkMode,\r\n type CSSWarning,\r\n} from \"@emailens/engine\";\r\nimport { readInput, resolveClients, resolveFormat, toFramework } from \"../utils.js\";\r\nimport { compile } from \"../compile/index.js\";\r\nimport { printScoreTable, printWarnings } from \"../output/terminal.js\";\r\nimport { printJson } from \"../output/json.js\";\r\n\r\nexport default defineCommand({\r\n meta: {\r\n name: \"preview\",\r\n description: \"Full email preview: transforms, analysis, dark mode, screenshots\",\r\n },\r\n args: {\r\n input: {\r\n type: \"positional\",\r\n description: \"HTML file path or - for stdin\",\r\n required: true,\r\n },\r\n format: {\r\n type: \"string\",\r\n alias: \"f\",\r\n description: \"Input format: html, jsx, mjml, maizzle\",\r\n },\r\n clients: {\r\n type: \"string\",\r\n alias: \"c\",\r\n description: \"Comma-separated client IDs to filter\",\r\n },\r\n \"dark-mode\": {\r\n type: \"boolean\",\r\n alias: \"d\",\r\n description: \"Include dark mode simulation\",\r\n },\r\n screenshots: {\r\n type: \"boolean\",\r\n description: \"Capture screenshots (requires BROWSERLESS_URL)\",\r\n },\r\n out: {\r\n type: \"string\",\r\n alias: \"o\",\r\n description: \"Output directory for screenshots\",\r\n },\r\n json: {\r\n type: \"boolean\",\r\n description: \"Output as JSON\",\r\n },\r\n quiet: {\r\n type: \"boolean\",\r\n alias: \"q\",\r\n description: \"Suppress spinners and decorations\",\r\n },\r\n },\r\n async run({ args }) {\r\n const spinner = (args.quiet || args.json) ? null : ora();\r\n\r\n try {\r\n // Resolve format early so we fail fast on invalid --format\r\n const format = resolveFormat(args.format, args.input);\r\n\r\n // Read input\r\n spinner?.start(\"Reading input...\");\r\n const source = await readInput(args.input);\r\n spinner?.succeed(\"Input read\");\r\n\r\n // Compile (passthrough for html, auto-detects from extension)\r\n const html = await compile(source, format, args.input !== \"-\" ? args.input : undefined);\r\n\r\n // Resolve clients\r\n const clientIds = resolveClients(args.clients);\r\n const clientSet = new Set(clientIds);\r\n\r\n // Transform\r\n spinner?.start(\"Transforming for all clients...\");\r\n const allTransforms = transformForAllClients(html);\r\n const transforms = allTransforms.filter((t) => clientSet.has(t.clientId));\r\n spinner?.succeed(`Transformed for ${transforms.length} clients`);\r\n\r\n // Analyze\r\n spinner?.start(\"Analyzing compatibility...\");\r\n const framework = toFramework(format);\r\n const warnings = analyzeEmail(html, framework);\r\n const allScores = generateCompatibilityScore(warnings);\r\n\r\n const scores: typeof allScores = {};\r\n for (const [id, data] of Object.entries(allScores)) {\r\n if (clientSet.has(id)) {\r\n scores[id] = data;\r\n }\r\n }\r\n const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));\r\n spinner?.succeed(\"Analysis complete\");\r\n\r\n // Dark mode\r\n let darkMode: Record<string, { html: string; warnings: CSSWarning[] }> | undefined;\r\n if (args[\"dark-mode\"]) {\r\n spinner?.start(\"Simulating dark mode...\");\r\n darkMode = {};\r\n for (const t of transforms) {\r\n darkMode[t.clientId] = simulateDarkMode(t.html, t.clientId);\r\n }\r\n spinner?.succeed(\"Dark mode simulation complete\");\r\n }\r\n\r\n // Screenshots\r\n if (args.screenshots) {\r\n const outDir = args.out ?? \"./emailens-screenshots\";\r\n spinner?.start(\"Capturing screenshots...\");\r\n try {\r\n const { captureScreenshots } = await import(\"../screenshots.js\");\r\n const results = await captureScreenshots(\r\n transforms,\r\n outDir,\r\n (clientId, index, total) => {\r\n if (spinner) spinner.text = `Capturing screenshots (${index}/${total}: ${clientId})...`;\r\n },\r\n );\r\n spinner?.succeed(`Screenshots saved to ${outDir} (${results.size} files)`);\r\n } catch (err) {\r\n spinner?.warn(`Screenshots skipped: ${(err as Error).message}`);\r\n }\r\n }\r\n\r\n // Output\r\n if (args.json) {\r\n printJson({ scores, warnings: filteredWarnings, transforms, darkMode });\r\n } else {\r\n printScoreTable(scores, filteredWarnings, { quiet: args.quiet });\r\n printWarnings(filteredWarnings, { quiet: args.quiet });\r\n }\r\n } catch (err) {\r\n spinner?.fail((err as Error).message);\r\n process.exit(1);\r\n }\r\n },\r\n});\r\n","import { defineCommand } from \"citty\";\r\nimport pc from \"picocolors\";\r\nimport Table from \"cli-table3\";\r\nimport { EMAIL_CLIENTS } from \"@emailens/engine\";\r\n\r\nexport default defineCommand({\r\n meta: {\r\n name: \"clients\",\r\n description: \"List all supported email clients\",\r\n },\r\n args: {\r\n json: {\r\n type: \"boolean\",\r\n description: \"Output as JSON\",\r\n },\r\n },\r\n run({ args }) {\r\n if (args.json) {\r\n const data = EMAIL_CLIENTS.map((c) => ({\r\n id: c.id,\r\n name: c.name,\r\n category: c.category,\r\n engine: c.engine,\r\n darkModeSupport: c.darkModeSupport,\r\n }));\r\n console.log(JSON.stringify(data, null, 2));\r\n return;\r\n }\r\n\r\n console.log();\r\n console.log(pc.bold(\" Emailens — Supported Email Clients\"));\r\n console.log();\r\n\r\n const table = new Table({\r\n head: [\r\n pc.bold(\"ID\"),\r\n pc.bold(\"Name\"),\r\n pc.bold(\"Category\"),\r\n pc.bold(\"Engine\"),\r\n pc.bold(\"Dark Mode\"),\r\n ],\r\n style: { head: [], border: [] },\r\n });\r\n\r\n for (const client of EMAIL_CLIENTS) {\r\n table.push([\r\n client.id,\r\n client.name,\r\n client.category,\r\n client.engine,\r\n client.darkModeSupport ? pc.green(\"Yes\") : pc.dim(\"No\"),\r\n ]);\r\n }\r\n\r\n console.log(table.toString());\r\n console.log();\r\n console.log(pc.dim(` ${EMAIL_CLIENTS.length} clients supported`));\r\n console.log();\r\n },\r\n});\r\n","import { mkdirSync, writeFileSync } from \"node:fs\";\r\nimport { join, resolve } from \"node:path\";\r\nimport { defineCommand } from \"citty\";\r\nimport ora from \"ora\";\r\nimport {\r\n transformForAllClients,\r\n analyzeEmail,\r\n generateCompatibilityScore,\r\n simulateDarkMode,\r\n type CSSWarning,\r\n} from \"@emailens/engine\";\r\nimport { readInput, resolveClients, resolveFormat, toFramework } from \"../utils.js\";\r\nimport { compile } from \"../compile/index.js\";\r\nimport { generateHtmlReport } from \"../output/html-report.js\";\r\nimport { formatJsonOutput } from \"../output/json.js\";\r\n\r\nexport default defineCommand({\r\n meta: {\r\n name: \"export\",\r\n description: \"Export compatibility report as JSON or HTML\",\r\n },\r\n args: {\r\n input: {\r\n type: \"positional\",\r\n description: \"HTML file path or - for stdin\",\r\n required: true,\r\n },\r\n format: {\r\n type: \"string\",\r\n alias: \"f\",\r\n description: \"Input format: html, jsx, mjml, maizzle\",\r\n },\r\n clients: {\r\n type: \"string\",\r\n alias: \"c\",\r\n description: \"Comma-separated client IDs to filter\",\r\n },\r\n \"dark-mode\": {\r\n type: \"boolean\",\r\n alias: \"d\",\r\n description: \"Include dark mode simulation\",\r\n },\r\n screenshots: {\r\n type: \"boolean\",\r\n description: \"Capture screenshots (requires BROWSERLESS_URL)\",\r\n },\r\n out: {\r\n type: \"string\",\r\n alias: \"o\",\r\n description: \"Output directory (default: ./emailens-report)\",\r\n },\r\n json: {\r\n type: \"boolean\",\r\n description: \"Export as JSON instead of HTML\",\r\n },\r\n quiet: {\r\n type: \"boolean\",\r\n alias: \"q\",\r\n description: \"Suppress spinners and decorations\",\r\n },\r\n },\r\n async run({ args }) {\r\n const spinner = args.quiet ? null : ora();\r\n const outDir = resolve(args.out ?? \"./emailens-report\");\r\n\r\n try {\r\n // Resolve format early so we fail fast on invalid --format\r\n const format = resolveFormat(args.format, args.input);\r\n\r\n // Read input\r\n spinner?.start(\"Reading input...\");\r\n const source = await readInput(args.input);\r\n spinner?.succeed(\"Input read\");\r\n\r\n // Compile (passthrough for html, auto-detects from extension)\r\n const html = await compile(source, format, args.input !== \"-\" ? args.input : undefined);\r\n\r\n // Resolve clients\r\n const clientIds = resolveClients(args.clients);\r\n const clientSet = new Set(clientIds);\r\n\r\n // Transform\r\n spinner?.start(\"Transforming for all clients...\");\r\n const allTransforms = transformForAllClients(html);\r\n const transforms = allTransforms.filter((t) => clientSet.has(t.clientId));\r\n spinner?.succeed(`Transformed for ${transforms.length} clients`);\r\n\r\n // Analyze\r\n spinner?.start(\"Analyzing compatibility...\");\r\n const framework = toFramework(format);\r\n const warnings = analyzeEmail(html, framework);\r\n const allScores = generateCompatibilityScore(warnings);\r\n\r\n const scores: typeof allScores = {};\r\n for (const [id, data] of Object.entries(allScores)) {\r\n if (clientSet.has(id)) {\r\n scores[id] = data;\r\n }\r\n }\r\n const filteredWarnings = warnings.filter((w) => clientSet.has(w.client));\r\n spinner?.succeed(\"Analysis complete\");\r\n\r\n // Dark mode\r\n let darkMode: Record<string, { html: string; warnings: CSSWarning[] }> | undefined;\r\n if (args[\"dark-mode\"]) {\r\n spinner?.start(\"Simulating dark mode...\");\r\n darkMode = {};\r\n for (const t of transforms) {\r\n darkMode[t.clientId] = simulateDarkMode(t.html, t.clientId);\r\n }\r\n spinner?.succeed(\"Dark mode simulation complete\");\r\n }\r\n\r\n // Screenshots\r\n if (args.screenshots) {\r\n spinner?.start(\"Capturing screenshots...\");\r\n try {\r\n const { captureScreenshots } = await import(\"../screenshots.js\");\r\n const screenshotDir = join(outDir, \"screenshots\");\r\n const results = await captureScreenshots(\r\n transforms,\r\n screenshotDir,\r\n (clientId, index, total) => {\r\n if (spinner) spinner.text = `Capturing screenshots (${index}/${total}: ${clientId})...`;\r\n },\r\n );\r\n spinner?.succeed(`Screenshots saved (${results.size} files)`);\r\n } catch (err) {\r\n spinner?.warn(`Screenshots skipped: ${(err as Error).message}`);\r\n }\r\n }\r\n\r\n // Export\r\n mkdirSync(outDir, { recursive: true });\r\n\r\n if (args.json) {\r\n spinner?.start(\"Exporting JSON...\");\r\n const jsonData = formatJsonOutput({ scores, warnings: filteredWarnings, transforms });\r\n const jsonPath = join(outDir, \"report.json\");\r\n writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));\r\n spinner?.succeed(`JSON report saved to ${jsonPath}`);\r\n } else {\r\n spinner?.start(\"Generating HTML report...\");\r\n const htmlReport = generateHtmlReport({\r\n scores,\r\n warnings: filteredWarnings,\r\n transforms,\r\n darkMode,\r\n originalHtml: html,\r\n });\r\n\r\n const htmlPath = join(outDir, \"report.html\");\r\n writeFileSync(htmlPath, htmlReport);\r\n spinner?.succeed(`HTML report saved to ${htmlPath}`);\r\n }\r\n\r\n if (!args.quiet) {\r\n console.log();\r\n console.log(` Output: ${outDir}`);\r\n console.log();\r\n }\r\n } catch (err) {\r\n spinner?.fail((err as Error).message);\r\n process.exit(1);\r\n }\r\n },\r\n});\r\n","import { EMAIL_CLIENTS, type CSSWarning, type TransformResult } from \"@emailens/engine\";\r\n\r\n/**\r\n * Generate a self-contained HTML report file.\r\n */\r\nexport function generateHtmlReport(opts: {\r\n scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;\r\n warnings: CSSWarning[];\r\n transforms?: TransformResult[];\r\n darkMode?: Record<string, { html: string; warnings: CSSWarning[] }>;\r\n originalHtml?: string;\r\n}): string {\r\n const clientMap = new Map(EMAIL_CLIENTS.map((c) => [c.id, c]));\r\n\r\n const scoreValues = Object.values(opts.scores);\r\n const overallScore = scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n const scoreColor = (s: number) => s >= 90 ? \"#22c55e\" : s >= 70 ? \"#eab308\" : \"#ef4444\";\r\n\r\n // Build client cards\r\n const clientCards = Object.entries(opts.scores)\r\n .sort(([, a], [, b]) => a.score - b.score)\r\n .map(([clientId, { score, errors, warnings: warns, info }]) => {\r\n const client = clientMap.get(clientId);\r\n const name = client?.name ?? clientId;\r\n const clientWarnings = opts.warnings.filter((w) => w.client === clientId);\r\n const issueCount = errors + warns + info;\r\n\r\n const warningRows = clientWarnings.map((w) => {\r\n const icon = w.severity === \"error\" ? \"✗\" : w.severity === \"warning\" ? \"⚠\" : \"ℹ\";\r\n const color = w.severity === \"error\" ? \"#ef4444\" : w.severity === \"warning\" ? \"#eab308\" : \"#3b82f6\";\r\n return `<div style=\"padding:4px 0;border-bottom:1px solid #f1f1f1\">\r\n <span style=\"color:${color}\">${icon}</span>\r\n <strong>${escapeHtml(w.property)}</strong> — ${escapeHtml(w.message)}\r\n ${w.suggestion ? `<br><span style=\"color:#888;margin-left:20px\">→ ${escapeHtml(w.suggestion)}</span>` : \"\"}\r\n </div>`;\r\n }).join(\"\");\r\n\r\n return `<div style=\"border:1px solid #e5e5e5;border-radius:8px;padding:16px;margin-bottom:16px\">\r\n <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px\">\r\n <h3 style=\"margin:0;font-size:16px\">${escapeHtml(name)}</h3>\r\n <span style=\"font-size:24px;font-weight:bold;color:${scoreColor(score)}\">${score}</span>\r\n </div>\r\n <div style=\"font-size:13px;color:#888;margin-bottom:8px\">${client?.engine ?? \"\"} · ${client?.category ?? \"\"} · ${issueCount} issue${issueCount === 1 ? \"\" : \"s\"}</div>\r\n ${clientWarnings.length > 0 ? `<div style=\"font-size:13px\">${warningRows}</div>` : '<div style=\"color:#22c55e;font-size:13px\">No issues</div>'}\r\n </div>`;\r\n }).join(\"\");\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n<meta charset=\"utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n<title>Emailens Compatibility Report</title>\r\n<style>\r\n * { box-sizing: border-box; }\r\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; margin: 0; padding: 24px; background: #fafafa; color: #333; }\r\n .container { max-width: 900px; margin: 0 auto; }\r\n .header { text-align: center; margin-bottom: 32px; }\r\n .header h1 { margin: 0 0 8px; font-size: 24px; }\r\n .overall { font-size: 48px; font-weight: bold; }\r\n .subtitle { color: #888; font-size: 14px; }\r\n .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 16px; }\r\n .footer { text-align: center; margin-top: 32px; color: #aaa; font-size: 12px; }\r\n</style>\r\n</head>\r\n<body>\r\n<div class=\"container\">\r\n <div class=\"header\">\r\n <h1>Emailens Compatibility Report</h1>\r\n <div class=\"overall\" style=\"color:${scoreColor(overallScore)}\">${overallScore}/100</div>\r\n <div class=\"subtitle\">${Object.keys(opts.scores).length} clients analyzed · ${opts.warnings.length} total issues</div>\r\n </div>\r\n <div class=\"grid\">\r\n ${clientCards}\r\n </div>\r\n <div class=\"footer\">\r\n Generated by <a href=\"https://github.com/emailens/cli\" style=\"color:#888\">@emailens/cli</a>\r\n </div>\r\n</div>\r\n</body>\r\n</html>`;\r\n}\r\n\r\nfunction escapeHtml(str: string): string {\r\n return str\r\n .replace(/&/g, \"&\")\r\n .replace(/</g, \"<\")\r\n .replace(/>/g, \">\")\r\n .replace(/\"/g, \""\");\r\n}\r\n"],"mappings":"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","names":["defineCommand","resolve","EMAIL_CLIENTS","defineCommand","ora","analyzeEmail","generateCompatibilityScore","defineCommand","ora","analyzeEmail","generateCompatibilityScore","defineCommand","pc","Table","EMAIL_CLIENTS","defineCommand","ora","transformForAllClients","analyzeEmail","generateCompatibilityScore","simulateDarkMode","EMAIL_CLIENTS","defineCommand","ora","transformForAllClients","analyzeEmail","generateCompatibilityScore","simulateDarkMode","defineCommand"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/compile/maizzle.ts
|
|
4
|
+
var MAX_SOURCE_SIZE = 512e3;
|
|
5
|
+
var COMPILE_TIMEOUT_MS = 15e3;
|
|
6
|
+
async function compileMaizzle(source) {
|
|
7
|
+
if (!source || !source.trim()) {
|
|
8
|
+
throw new Error("Maizzle source must not be empty.");
|
|
9
|
+
}
|
|
10
|
+
if (source.length > MAX_SOURCE_SIZE) {
|
|
11
|
+
throw new Error(`Maizzle source exceeds ${MAX_SOURCE_SIZE / 1e3}KB limit.`);
|
|
12
|
+
}
|
|
13
|
+
let maizzleRender;
|
|
14
|
+
try {
|
|
15
|
+
const maizzle = await import("@maizzle/framework");
|
|
16
|
+
maizzleRender = maizzle.render;
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Maizzle compilation requires "@maizzle/framework". Install it:\n npm install @maizzle/framework'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const compilePromise = maizzleRender(source, {
|
|
23
|
+
css: {
|
|
24
|
+
inline: {
|
|
25
|
+
removeInlinedSelectors: true,
|
|
26
|
+
applyWidthAttributes: true,
|
|
27
|
+
applyHeightAttributes: true
|
|
28
|
+
},
|
|
29
|
+
shorthand: true,
|
|
30
|
+
sixHex: true
|
|
31
|
+
},
|
|
32
|
+
locals: {}
|
|
33
|
+
});
|
|
34
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
35
|
+
const t = setTimeout(() => {
|
|
36
|
+
reject(new Error(`Maizzle compilation timed out after ${COMPILE_TIMEOUT_MS / 1e3}s.`));
|
|
37
|
+
}, COMPILE_TIMEOUT_MS);
|
|
38
|
+
if (typeof t.unref === "function") t.unref();
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
const { html } = await Promise.race([compilePromise, timeoutPromise]);
|
|
42
|
+
if (!html) {
|
|
43
|
+
throw new Error("Maizzle compilation produced empty output.");
|
|
44
|
+
}
|
|
45
|
+
return html;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err instanceof Error && err.message.startsWith("Maizzle")) throw err;
|
|
48
|
+
const message = err instanceof Error ? err.message : "Unknown Maizzle compilation error";
|
|
49
|
+
throw new Error(`Maizzle compilation failed: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
compileMaizzle
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=maizzle-JGKTYDDO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compile/maizzle.ts"],"sourcesContent":["/** Maximum Maizzle source size: 512KB */\r\nconst MAX_SOURCE_SIZE = 512_000;\r\n\r\n/** Compilation timeout: 15 seconds */\r\nconst COMPILE_TIMEOUT_MS = 15_000;\r\n\r\n/**\r\n * Compile a Maizzle template string into an HTML email string.\r\n * Requires: @maizzle/framework (optional peer dependency)\r\n *\r\n * No dangerous-directive check in CLI — runs locally on the user's machine.\r\n */\r\nexport async function compileMaizzle(source: string): Promise<string> {\r\n // ── Validate ──────────────────────────────────────────────────────\r\n if (!source || !source.trim()) {\r\n throw new Error(\"Maizzle source must not be empty.\");\r\n }\r\n\r\n if (source.length > MAX_SOURCE_SIZE) {\r\n throw new Error(`Maizzle source exceeds ${MAX_SOURCE_SIZE / 1000}KB limit.`);\r\n }\r\n\r\n // ── Load optional dependency ──────────────────────────────────────\r\n let maizzleRender: (input: string, options: Record<string, unknown>) => Promise<{ html: string }>;\r\n try {\r\n const maizzle = await import(\"@maizzle/framework\");\r\n maizzleRender = maizzle.render;\r\n } catch {\r\n throw new Error(\r\n 'Maizzle compilation requires \"@maizzle/framework\". Install it:\\n npm install @maizzle/framework'\r\n );\r\n }\r\n\r\n // ── Compile with timeout ──────────────────────────────────────────\r\n const compilePromise = maizzleRender(source, {\r\n css: {\r\n inline: {\r\n removeInlinedSelectors: true,\r\n applyWidthAttributes: true,\r\n applyHeightAttributes: true,\r\n },\r\n shorthand: true,\r\n sixHex: true,\r\n },\r\n locals: {},\r\n });\r\n\r\n const timeoutPromise = new Promise<never>((_, reject) => {\r\n const t = setTimeout(() => {\r\n reject(new Error(`Maizzle compilation timed out after ${COMPILE_TIMEOUT_MS / 1000}s.`));\r\n }, COMPILE_TIMEOUT_MS);\r\n if (typeof t.unref === \"function\") t.unref();\r\n });\r\n\r\n try {\r\n const { html } = await Promise.race([compilePromise, timeoutPromise]);\r\n\r\n if (!html) {\r\n throw new Error(\"Maizzle compilation produced empty output.\");\r\n }\r\n\r\n return html;\r\n } catch (err: unknown) {\r\n if (err instanceof Error && err.message.startsWith(\"Maizzle\")) throw err;\r\n const message = err instanceof Error ? err.message : \"Unknown Maizzle compilation error\";\r\n throw new Error(`Maizzle compilation failed: ${message}`);\r\n }\r\n}\r\n"],"mappings":";;;AACA,IAAM,kBAAkB;AAGxB,IAAM,qBAAqB;AAQ3B,eAAsB,eAAe,QAAiC;AAEpE,MAAI,CAAC,UAAU,CAAC,OAAO,KAAK,GAAG;AAC7B,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,MAAI,OAAO,SAAS,iBAAiB;AACnC,UAAM,IAAI,MAAM,0BAA0B,kBAAkB,GAAI,WAAW;AAAA,EAC7E;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,oBAAgB,QAAQ;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiB,cAAc,QAAQ;AAAA,IAC3C,KAAK;AAAA,MACH,QAAQ;AAAA,QACN,wBAAwB;AAAA,QACxB,sBAAsB;AAAA,QACtB,uBAAuB;AAAA,MACzB;AAAA,MACA,WAAW;AAAA,MACX,QAAQ;AAAA,IACV;AAAA,IACA,QAAQ,CAAC;AAAA,EACX,CAAC;AAED,QAAM,iBAAiB,IAAI,QAAe,CAAC,GAAG,WAAW;AACvD,UAAM,IAAI,WAAW,MAAM;AACzB,aAAO,IAAI,MAAM,uCAAuC,qBAAqB,GAAI,IAAI,CAAC;AAAA,IACxF,GAAG,kBAAkB;AACrB,QAAI,OAAO,EAAE,UAAU,WAAY,GAAE,MAAM;AAAA,EAC7C,CAAC;AAED,MAAI;AACF,UAAM,EAAE,KAAK,IAAI,MAAM,QAAQ,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAEpE,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAEA,WAAO;AAAA,EACT,SAAS,KAAc;AACrB,QAAI,eAAe,SAAS,IAAI,QAAQ,WAAW,SAAS,EAAG,OAAM;AACrE,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,UAAM,IAAI,MAAM,+BAA+B,OAAO,EAAE;AAAA,EAC1D;AACF;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/compile/mjml.ts
|
|
4
|
+
var MAX_SOURCE_SIZE = 512e3;
|
|
5
|
+
async function compileMjml(source) {
|
|
6
|
+
if (!source || !source.trim()) {
|
|
7
|
+
throw new Error("MJML source must not be empty.");
|
|
8
|
+
}
|
|
9
|
+
if (source.length > MAX_SOURCE_SIZE) {
|
|
10
|
+
throw new Error(`MJML source exceeds ${MAX_SOURCE_SIZE / 1e3}KB limit.`);
|
|
11
|
+
}
|
|
12
|
+
if (!/<mjml[\s>]/i.test(source)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"MJML source must contain a root <mjml> element. Example: <mjml><mj-body><mj-section><mj-column><mj-text>Hello</mj-text></mj-column></mj-section></mj-body></mjml>"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
let mjml2html;
|
|
18
|
+
try {
|
|
19
|
+
const mjmlModule = await import("mjml");
|
|
20
|
+
mjml2html = mjmlModule.default ?? mjmlModule;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'MJML compilation requires "mjml". Install it:\n npm install mjml'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const result = mjml2html(source, {
|
|
28
|
+
validationLevel: "soft",
|
|
29
|
+
keepComments: false
|
|
30
|
+
});
|
|
31
|
+
if (result.errors && result.errors.length > 0 && !result.html) {
|
|
32
|
+
const errorMessages = result.errors.map((e) => `Line ${e.line}: ${e.message} (${e.tagName})`).join("; ");
|
|
33
|
+
throw new Error(`MJML compilation errors: ${errorMessages}`);
|
|
34
|
+
}
|
|
35
|
+
if (!result.html) {
|
|
36
|
+
throw new Error("MJML compilation produced empty output.");
|
|
37
|
+
}
|
|
38
|
+
return result.html;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof Error && err.message.startsWith("MJML")) throw err;
|
|
41
|
+
const message = err instanceof Error ? err.message : "Unknown MJML compilation error";
|
|
42
|
+
throw new Error(`MJML compilation failed: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
compileMjml
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=mjml-KG66APFB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compile/mjml.ts"],"sourcesContent":["/** Maximum MJML source size: 512KB */\r\nconst MAX_SOURCE_SIZE = 512_000;\r\n\r\n/**\r\n * Compile an MJML source string into an HTML email string.\r\n * Requires: mjml (optional peer dependency)\r\n */\r\nexport async function compileMjml(source: string): Promise<string> {\r\n // ── Validate ──────────────────────────────────────────────────────\r\n if (!source || !source.trim()) {\r\n throw new Error(\"MJML source must not be empty.\");\r\n }\r\n\r\n if (source.length > MAX_SOURCE_SIZE) {\r\n throw new Error(`MJML source exceeds ${MAX_SOURCE_SIZE / 1000}KB limit.`);\r\n }\r\n\r\n if (!/<mjml[\\s>]/i.test(source)) {\r\n throw new Error(\r\n \"MJML source must contain a root <mjml> element. \" +\r\n \"Example: <mjml><mj-body><mj-section><mj-column><mj-text>Hello</mj-text></mj-column></mj-section></mj-body></mjml>\"\r\n );\r\n }\r\n\r\n // ── Load optional dependency ──────────────────────────────────────\r\n let mjml2html: (input: string, options?: Record<string, unknown>) => { html: string; errors: Array<{ line: number; message: string; tagName: string; formattedMessage: string }> };\r\n try {\r\n const mjmlModule = await import(\"mjml\");\r\n mjml2html = mjmlModule.default ?? mjmlModule;\r\n } catch {\r\n throw new Error(\r\n 'MJML compilation requires \"mjml\". Install it:\\n npm install mjml'\r\n );\r\n }\r\n\r\n // ── Compile ───────────────────────────────────────────────────────\r\n try {\r\n const result = mjml2html(source, {\r\n validationLevel: \"soft\",\r\n keepComments: false,\r\n });\r\n\r\n if (result.errors && result.errors.length > 0 && !result.html) {\r\n const errorMessages = result.errors\r\n .map((e) => `Line ${e.line}: ${e.message} (${e.tagName})`)\r\n .join(\"; \");\r\n throw new Error(`MJML compilation errors: ${errorMessages}`);\r\n }\r\n\r\n if (!result.html) {\r\n throw new Error(\"MJML compilation produced empty output.\");\r\n }\r\n\r\n return result.html;\r\n } catch (err: unknown) {\r\n if (err instanceof Error && err.message.startsWith(\"MJML\")) throw err;\r\n const message = err instanceof Error ? err.message : \"Unknown MJML compilation error\";\r\n throw new Error(`MJML compilation failed: ${message}`);\r\n }\r\n}\r\n"],"mappings":";;;AACA,IAAM,kBAAkB;AAMxB,eAAsB,YAAY,QAAiC;AAEjE,MAAI,CAAC,UAAU,CAAC,OAAO,KAAK,GAAG;AAC7B,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAEA,MAAI,OAAO,SAAS,iBAAiB;AACnC,UAAM,IAAI,MAAM,uBAAuB,kBAAkB,GAAI,WAAW;AAAA,EAC1E;AAEA,MAAI,CAAC,cAAc,KAAK,MAAM,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,aAAa,MAAM,OAAO,MAAM;AACtC,gBAAY,WAAW,WAAW;AAAA,EACpC,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACF,UAAM,SAAS,UAAU,QAAQ;AAAA,MAC/B,iBAAiB;AAAA,MACjB,cAAc;AAAA,IAChB,CAAC;AAED,QAAI,OAAO,UAAU,OAAO,OAAO,SAAS,KAAK,CAAC,OAAO,MAAM;AAC7D,YAAM,gBAAgB,OAAO,OAC1B,IAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,KAAK,EAAE,OAAO,KAAK,EAAE,OAAO,GAAG,EACxD,KAAK,IAAI;AACZ,YAAM,IAAI,MAAM,4BAA4B,aAAa,EAAE;AAAA,IAC7D;AAEA,QAAI,CAAC,OAAO,MAAM;AAChB,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAEA,WAAO,OAAO;AAAA,EAChB,SAAS,KAAc;AACrB,QAAI,eAAe,SAAS,IAAI,QAAQ,WAAW,MAAM,EAAG,OAAM;AAClE,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,UAAM,IAAI,MAAM,4BAA4B,OAAO,EAAE;AAAA,EACvD;AACF;","names":[]}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/compile/react-email.ts
|
|
4
|
+
import { createContext, Script } from "vm";
|
|
5
|
+
var MAX_SOURCE_SIZE = 256e3;
|
|
6
|
+
var EXECUTION_TIMEOUT_MS = 5e3;
|
|
7
|
+
async function compileReactEmail(source) {
|
|
8
|
+
if (!source || !source.trim()) {
|
|
9
|
+
throw new Error("JSX source must not be empty.");
|
|
10
|
+
}
|
|
11
|
+
if (source.length > MAX_SOURCE_SIZE) {
|
|
12
|
+
throw new Error(`JSX source exceeds ${MAX_SOURCE_SIZE / 1e3}KB limit.`);
|
|
13
|
+
}
|
|
14
|
+
let transform;
|
|
15
|
+
let React;
|
|
16
|
+
let ReactEmailComponents;
|
|
17
|
+
let render;
|
|
18
|
+
try {
|
|
19
|
+
({ transform } = await import("sucrase"));
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'JSX compilation requires "sucrase". Install it:\n npm install sucrase'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
React = await import("react");
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'JSX compilation requires "react". Install it:\n npm install react'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
ReactEmailComponents = await import("@react-email/components");
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'JSX compilation requires "@react-email/components". Install it:\n npm install @react-email/components'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
({ render } = await import("@react-email/render"));
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'JSX compilation requires "@react-email/render". Install it:\n npm install @react-email/render'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
let transpiledCode;
|
|
47
|
+
try {
|
|
48
|
+
const result = transform(source, {
|
|
49
|
+
transforms: ["typescript", "jsx", "imports"],
|
|
50
|
+
jsxRuntime: "classic",
|
|
51
|
+
production: true
|
|
52
|
+
});
|
|
53
|
+
transpiledCode = result.code;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : "Unknown transpilation error";
|
|
56
|
+
throw new Error(`JSX syntax error: ${message}`);
|
|
57
|
+
}
|
|
58
|
+
const ALLOWED_MODULES = {
|
|
59
|
+
react: React,
|
|
60
|
+
"@react-email/components": ReactEmailComponents
|
|
61
|
+
};
|
|
62
|
+
const moduleExports = {};
|
|
63
|
+
const moduleObj = { exports: moduleExports };
|
|
64
|
+
const mockRequire = (moduleName) => {
|
|
65
|
+
if (moduleName in ALLOWED_MODULES) {
|
|
66
|
+
return ALLOWED_MODULES[moduleName];
|
|
67
|
+
}
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Import of "${moduleName}" is not allowed. Only "react" and "@react-email/components" can be imported.`
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
const sandbox = {
|
|
73
|
+
module: moduleObj,
|
|
74
|
+
exports: moduleExports,
|
|
75
|
+
require: mockRequire,
|
|
76
|
+
React,
|
|
77
|
+
Object,
|
|
78
|
+
Array,
|
|
79
|
+
String,
|
|
80
|
+
Number,
|
|
81
|
+
Boolean,
|
|
82
|
+
Map,
|
|
83
|
+
Set,
|
|
84
|
+
WeakMap,
|
|
85
|
+
WeakSet,
|
|
86
|
+
JSON,
|
|
87
|
+
Math,
|
|
88
|
+
Date,
|
|
89
|
+
RegExp,
|
|
90
|
+
Error,
|
|
91
|
+
TypeError,
|
|
92
|
+
RangeError,
|
|
93
|
+
ReferenceError,
|
|
94
|
+
SyntaxError,
|
|
95
|
+
URIError,
|
|
96
|
+
Promise,
|
|
97
|
+
Symbol,
|
|
98
|
+
Proxy,
|
|
99
|
+
Reflect,
|
|
100
|
+
parseInt,
|
|
101
|
+
parseFloat,
|
|
102
|
+
isNaN,
|
|
103
|
+
isFinite,
|
|
104
|
+
encodeURIComponent,
|
|
105
|
+
decodeURIComponent,
|
|
106
|
+
encodeURI,
|
|
107
|
+
decodeURI,
|
|
108
|
+
undefined: void 0,
|
|
109
|
+
NaN: NaN,
|
|
110
|
+
Infinity: Infinity,
|
|
111
|
+
console: { log: () => {
|
|
112
|
+
}, warn: () => {
|
|
113
|
+
}, error: () => {
|
|
114
|
+
}, info: () => {
|
|
115
|
+
}, debug: () => {
|
|
116
|
+
} },
|
|
117
|
+
setTimeout: void 0,
|
|
118
|
+
setInterval: void 0,
|
|
119
|
+
setImmediate: void 0,
|
|
120
|
+
queueMicrotask: void 0,
|
|
121
|
+
process: void 0,
|
|
122
|
+
globalThis: void 0,
|
|
123
|
+
global: void 0,
|
|
124
|
+
Buffer: void 0,
|
|
125
|
+
__dirname: void 0,
|
|
126
|
+
__filename: void 0
|
|
127
|
+
};
|
|
128
|
+
const context = createContext(sandbox, {
|
|
129
|
+
codeGeneration: { strings: false, wasm: false }
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const script = new Script(transpiledCode, { filename: "user-email-component.tsx" });
|
|
133
|
+
script.runInContext(context, { timeout: EXECUTION_TIMEOUT_MS, displayErrors: true });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const message = err instanceof Error ? err.message : "Unknown execution error";
|
|
136
|
+
if (message.includes("Script execution timed out")) {
|
|
137
|
+
throw new Error("JSX execution timed out (possible infinite loop).");
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`JSX execution error: ${message}`);
|
|
140
|
+
}
|
|
141
|
+
let Component = moduleObj.exports.default ?? moduleObj.exports;
|
|
142
|
+
if (typeof Component !== "function" && typeof Component === "object" && Component !== null) {
|
|
143
|
+
const values = Object.values(Component);
|
|
144
|
+
const fn = values.find((v) => typeof v === "function");
|
|
145
|
+
if (fn) Component = fn;
|
|
146
|
+
}
|
|
147
|
+
if (typeof Component !== "function") {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'The JSX source must export a React component function. Use "export default function Email() { ... }" or "export function Email() { ... }".'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const element = React.createElement(Component);
|
|
154
|
+
const html = await render(element);
|
|
155
|
+
return html;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : "Unknown rendering error";
|
|
158
|
+
throw new Error(`React rendering error: ${message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
compileReactEmail
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=react-email-ZWLGXROP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compile/react-email.ts"],"sourcesContent":["import { createContext, Script } from \"node:vm\";\r\n\r\n/** Maximum source code size: 256KB */\r\nconst MAX_SOURCE_SIZE = 256_000;\r\n\r\n/** Execution timeout: 5 seconds */\r\nconst EXECUTION_TIMEOUT_MS = 5_000;\r\n\r\n/**\r\n * Compile a React Email JSX/TSX source string into an HTML email string.\r\n *\r\n * Ported from the webapp compiler, simplified for CLI use.\r\n * Uses sucrase for JSX transpilation and vm sandbox for safe execution.\r\n * Requires: sucrase, react, @react-email/components, @react-email/render\r\n */\r\nexport async function compileReactEmail(source: string): Promise<string> {\r\n // ── Validate ──────────────────────────────────────────────────────\r\n if (!source || !source.trim()) {\r\n throw new Error(\"JSX source must not be empty.\");\r\n }\r\n\r\n if (source.length > MAX_SOURCE_SIZE) {\r\n throw new Error(`JSX source exceeds ${MAX_SOURCE_SIZE / 1000}KB limit.`);\r\n }\r\n\r\n // ── Load optional dependencies ────────────────────────────────────\r\n let transform: typeof import(\"sucrase\").transform;\r\n let React: typeof import(\"react\");\r\n let ReactEmailComponents: typeof import(\"@react-email/components\");\r\n let render: typeof import(\"@react-email/render\").render;\r\n\r\n try {\r\n ({ transform } = await import(\"sucrase\"));\r\n } catch {\r\n throw new Error(\r\n 'JSX compilation requires \"sucrase\". Install it:\\n npm install sucrase'\r\n );\r\n }\r\n\r\n try {\r\n React = await import(\"react\");\r\n } catch {\r\n throw new Error(\r\n 'JSX compilation requires \"react\". Install it:\\n npm install react'\r\n );\r\n }\r\n\r\n try {\r\n ReactEmailComponents = await import(\"@react-email/components\");\r\n } catch {\r\n throw new Error(\r\n 'JSX compilation requires \"@react-email/components\". Install it:\\n npm install @react-email/components'\r\n );\r\n }\r\n\r\n try {\r\n ({ render } = await import(\"@react-email/render\"));\r\n } catch {\r\n throw new Error(\r\n 'JSX compilation requires \"@react-email/render\". Install it:\\n npm install @react-email/render'\r\n );\r\n }\r\n\r\n // ── Transpile JSX/TSX → CommonJS ──────────────────────────────────\r\n let transpiledCode: string;\r\n try {\r\n const result = transform(source, {\r\n transforms: [\"typescript\", \"jsx\", \"imports\"],\r\n jsxRuntime: \"classic\",\r\n production: true,\r\n });\r\n transpiledCode = result.code;\r\n } catch (err: unknown) {\r\n const message = err instanceof Error ? err.message : \"Unknown transpilation error\";\r\n throw new Error(`JSX syntax error: ${message}`);\r\n }\r\n\r\n // ── Execute in sandbox ────────────────────────────────────────────\r\n const ALLOWED_MODULES: Record<string, unknown> = {\r\n react: React,\r\n \"@react-email/components\": ReactEmailComponents,\r\n };\r\n\r\n const moduleExports: Record<string, unknown> = {};\r\n const moduleObj = { exports: moduleExports };\r\n\r\n const mockRequire = (moduleName: string): unknown => {\r\n if (moduleName in ALLOWED_MODULES) {\r\n return ALLOWED_MODULES[moduleName];\r\n }\r\n throw new Error(\r\n `Import of \"${moduleName}\" is not allowed. ` +\r\n `Only \"react\" and \"@react-email/components\" can be imported.`\r\n );\r\n };\r\n\r\n const sandbox: Record<string, unknown> = {\r\n module: moduleObj,\r\n exports: moduleExports,\r\n require: mockRequire,\r\n React,\r\n Object, Array, String, Number, Boolean,\r\n Map, Set, WeakMap, WeakSet,\r\n JSON, Math, Date, RegExp,\r\n Error, TypeError, RangeError, ReferenceError, SyntaxError, URIError,\r\n Promise, Symbol, Proxy, Reflect,\r\n parseInt, parseFloat, isNaN, isFinite,\r\n encodeURIComponent, decodeURIComponent, encodeURI, decodeURI,\r\n undefined, NaN, Infinity,\r\n console: { log: () => {}, warn: () => {}, error: () => {}, info: () => {}, debug: () => {} },\r\n setTimeout: undefined, setInterval: undefined, setImmediate: undefined, queueMicrotask: undefined,\r\n process: undefined, globalThis: undefined, global: undefined, Buffer: undefined,\r\n __dirname: undefined, __filename: undefined,\r\n };\r\n\r\n const context = createContext(sandbox, {\r\n codeGeneration: { strings: false, wasm: false },\r\n });\r\n\r\n try {\r\n const script = new Script(transpiledCode, { filename: \"user-email-component.tsx\" });\r\n script.runInContext(context, { timeout: EXECUTION_TIMEOUT_MS, displayErrors: true });\r\n } catch (err: unknown) {\r\n const message = err instanceof Error ? err.message : \"Unknown execution error\";\r\n if (message.includes(\"Script execution timed out\")) {\r\n throw new Error(\"JSX execution timed out (possible infinite loop).\");\r\n }\r\n throw new Error(`JSX execution error: ${message}`);\r\n }\r\n\r\n // ── Extract component and render ──────────────────────────────────\r\n let Component: unknown = moduleObj.exports.default ?? moduleObj.exports;\r\n\r\n if (typeof Component !== \"function\" && typeof Component === \"object\" && Component !== null) {\r\n const values = Object.values(Component as Record<string, unknown>);\r\n const fn = values.find((v) => typeof v === \"function\");\r\n if (fn) Component = fn;\r\n }\r\n\r\n if (typeof Component !== \"function\") {\r\n throw new Error(\r\n 'The JSX source must export a React component function. ' +\r\n 'Use \"export default function Email() { ... }\" or ' +\r\n '\"export function Email() { ... }\".'\r\n );\r\n }\r\n\r\n try {\r\n const element = React.createElement(Component as () => unknown);\r\n const html = await render(element);\r\n return html;\r\n } catch (err: unknown) {\r\n const message = err instanceof Error ? err.message : \"Unknown rendering error\";\r\n throw new Error(`React rendering error: ${message}`);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/screenshots.ts
|
|
4
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var CONNECT_TIMEOUT_MS = 15e3;
|
|
7
|
+
var PAGE_TIMEOUT_MS = 3e4;
|
|
8
|
+
async function captureScreenshots(transforms, outputDir, onProgress) {
|
|
9
|
+
const browserlessUrl = process.env.BROWSERLESS_URL;
|
|
10
|
+
if (!browserlessUrl) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Screenshots require the BROWSERLESS_URL environment variable.\nSet it to your Browserless instance URL (e.g. ws://localhost:3000)."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
let chromium;
|
|
16
|
+
try {
|
|
17
|
+
const pw = await import("playwright-core");
|
|
18
|
+
chromium = pw.chromium;
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Screenshots require "playwright-core". Install it:\n npm install playwright-core'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
mkdirSync(outputDir, { recursive: true });
|
|
25
|
+
const results = /* @__PURE__ */ new Map();
|
|
26
|
+
let browser;
|
|
27
|
+
try {
|
|
28
|
+
browser = await Promise.race([
|
|
29
|
+
chromium.connectOverCDP(browserlessUrl),
|
|
30
|
+
new Promise((_, reject) => {
|
|
31
|
+
const t = setTimeout(() => {
|
|
32
|
+
reject(new Error(`Browserless connection timed out after ${CONNECT_TIMEOUT_MS / 1e3}s. Check BROWSERLESS_URL.`));
|
|
33
|
+
}, CONNECT_TIMEOUT_MS);
|
|
34
|
+
if (typeof t.unref === "function") t.unref();
|
|
35
|
+
})
|
|
36
|
+
]);
|
|
37
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
38
|
+
for (let i = 0; i < transforms.length; i++) {
|
|
39
|
+
const { clientId, html } = transforms[i];
|
|
40
|
+
onProgress?.(clientId, i + 1, transforms.length);
|
|
41
|
+
const page = await context.newPage();
|
|
42
|
+
page.setDefaultTimeout(PAGE_TIMEOUT_MS);
|
|
43
|
+
await page.setViewportSize({ width: 600, height: 800 });
|
|
44
|
+
await page.setContent(html, { waitUntil: "networkidle" });
|
|
45
|
+
const screenshotPath = join(outputDir, `${clientId}.png`);
|
|
46
|
+
const buffer = await page.screenshot({ fullPage: true });
|
|
47
|
+
writeFileSync(screenshotPath, buffer);
|
|
48
|
+
results.set(clientId, screenshotPath);
|
|
49
|
+
await page.close();
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
if (browser) {
|
|
53
|
+
await browser.close().catch(() => {
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
captureScreenshots
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=screenshots-PMZQU5V7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/screenshots.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\r\nimport { join } from \"node:path\";\r\n\r\n/** CDP connection timeout: 15 seconds */\r\nconst CONNECT_TIMEOUT_MS = 15_000;\r\n\r\n/** Per-page screenshot timeout: 30 seconds */\r\nconst PAGE_TIMEOUT_MS = 30_000;\r\n\r\n/**\r\n * Capture screenshots of email transforms using Browserless via CDP.\r\n * Requires: playwright-core (optional) + BROWSERLESS_URL env var.\r\n *\r\n * Returns map of clientId → screenshot file path.\r\n */\r\nexport async function captureScreenshots(\r\n transforms: Array<{ clientId: string; html: string }>,\r\n outputDir: string,\r\n onProgress?: (clientId: string, index: number, total: number) => void,\r\n): Promise<Map<string, string>> {\r\n const browserlessUrl = process.env.BROWSERLESS_URL;\r\n if (!browserlessUrl) {\r\n throw new Error(\r\n \"Screenshots require the BROWSERLESS_URL environment variable.\\n\" +\r\n \"Set it to your Browserless instance URL (e.g. ws://localhost:3000).\"\r\n );\r\n }\r\n\r\n let chromium: typeof import(\"playwright-core\").chromium;\r\n try {\r\n const pw = await import(\"playwright-core\");\r\n chromium = pw.chromium;\r\n } catch {\r\n throw new Error(\r\n 'Screenshots require \"playwright-core\". Install it:\\n npm install playwright-core'\r\n );\r\n }\r\n\r\n // Ensure output directory exists\r\n mkdirSync(outputDir, { recursive: true });\r\n\r\n const results = new Map<string, string>();\r\n let browser;\r\n\r\n try {\r\n // Connect with timeout to avoid hanging on unreachable Browserless\r\n browser = await Promise.race([\r\n chromium.connectOverCDP(browserlessUrl),\r\n new Promise<never>((_, reject) => {\r\n const t = setTimeout(() => {\r\n reject(new Error(`Browserless connection timed out after ${CONNECT_TIMEOUT_MS / 1000}s. Check BROWSERLESS_URL.`));\r\n }, CONNECT_TIMEOUT_MS);\r\n if (typeof t.unref === \"function\") t.unref();\r\n }),\r\n ]);\r\n\r\n const context = browser.contexts()[0] ?? await browser.newContext();\r\n\r\n for (let i = 0; i < transforms.length; i++) {\r\n const { clientId, html } = transforms[i];\r\n onProgress?.(clientId, i + 1, transforms.length);\r\n\r\n const page = await context.newPage();\r\n page.setDefaultTimeout(PAGE_TIMEOUT_MS);\r\n await page.setViewportSize({ width: 600, height: 800 });\r\n await page.setContent(html, { waitUntil: \"networkidle\" });\r\n\r\n const screenshotPath = join(outputDir, `${clientId}.png`);\r\n const buffer = await page.screenshot({ fullPage: true });\r\n writeFileSync(screenshotPath, buffer);\r\n results.set(clientId, screenshotPath);\r\n\r\n await page.close();\r\n }\r\n } finally {\r\n if (browser) {\r\n await browser.close().catch(() => {});\r\n }\r\n }\r\n\r\n return results;\r\n}\r\n"],"mappings":";;;AAAA,SAAS,WAAW,qBAAqB;AACzC,SAAS,YAAY;AAGrB,IAAM,qBAAqB;AAG3B,IAAM,kBAAkB;AAQxB,eAAsB,mBACpB,YACA,WACA,YAC8B;AAC9B,QAAM,iBAAiB,QAAQ,IAAI;AACnC,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO,iBAAiB;AACzC,eAAW,GAAG;AAAA,EAChB,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,YAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAExC,QAAM,UAAU,oBAAI,IAAoB;AACxC,MAAI;AAEJ,MAAI;AAEF,cAAU,MAAM,QAAQ,KAAK;AAAA,MAC3B,SAAS,eAAe,cAAc;AAAA,MACtC,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,cAAM,IAAI,WAAW,MAAM;AACzB,iBAAO,IAAI,MAAM,0CAA0C,qBAAqB,GAAI,2BAA2B,CAAC;AAAA,QAClH,GAAG,kBAAkB;AACrB,YAAI,OAAO,EAAE,UAAU,WAAY,GAAE,MAAM;AAAA,MAC7C,CAAC;AAAA,IACH,CAAC;AAED,UAAM,UAAU,QAAQ,SAAS,EAAE,CAAC,KAAK,MAAM,QAAQ,WAAW;AAElE,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,YAAM,EAAE,UAAU,KAAK,IAAI,WAAW,CAAC;AACvC,mBAAa,UAAU,IAAI,GAAG,WAAW,MAAM;AAE/C,YAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,WAAK,kBAAkB,eAAe;AACtC,YAAM,KAAK,gBAAgB,EAAE,OAAO,KAAK,QAAQ,IAAI,CAAC;AACtD,YAAM,KAAK,WAAW,MAAM,EAAE,WAAW,cAAc,CAAC;AAExD,YAAM,iBAAiB,KAAK,WAAW,GAAG,QAAQ,MAAM;AACxD,YAAM,SAAS,MAAM,KAAK,WAAW,EAAE,UAAU,KAAK,CAAC;AACvD,oBAAc,gBAAgB,MAAM;AACpC,cAAQ,IAAI,UAAU,cAAc;AAEpC,YAAM,KAAK,MAAM;AAAA,IACnB;AAAA,EACF,UAAE;AACA,QAAI,SAAS;AACX,YAAM,QAAQ,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACtC;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emailens/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for email compatibility analysis — analyze, preview, and export email rendering across 12 email clients.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"emailens": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"start": "bun run dist/index.js",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@emailens/engine": "^0.1.0",
|
|
31
|
+
"citty": "^0.1.6",
|
|
32
|
+
"cli-table3": "^0.6.5",
|
|
33
|
+
"ora": "^8.2.0",
|
|
34
|
+
"picocolors": "^1.1.1"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"sucrase": "^3.35.0",
|
|
38
|
+
"@react-email/components": "^0.0.36",
|
|
39
|
+
"@react-email/render": "^1.0.6",
|
|
40
|
+
"mjml": "^5.0.0-alpha.4",
|
|
41
|
+
"@maizzle/framework": "^5.0.0",
|
|
42
|
+
"playwright-core": "^1.52.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/mjml": "^4.7.4",
|
|
46
|
+
"@types/node": "^25.3.0",
|
|
47
|
+
"bun-types": "^1.3.9",
|
|
48
|
+
"tsup": "^8.4.0",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"email",
|
|
53
|
+
"html-email",
|
|
54
|
+
"css",
|
|
55
|
+
"compatibility",
|
|
56
|
+
"cli",
|
|
57
|
+
"gmail",
|
|
58
|
+
"outlook",
|
|
59
|
+
"apple-mail",
|
|
60
|
+
"email-testing",
|
|
61
|
+
"dark-mode"
|
|
62
|
+
],
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/emailens/cli.git"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/emailens/cli#readme",
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/emailens/cli/issues"
|
|
71
|
+
}
|
|
72
|
+
}
|