From 56348368efefbfb208a76c27a095410bee34ece1 Mon Sep 17 00:00:00 2001 From: shellscape Date: Sun, 17 May 2026 17:32:39 -0400 Subject: [PATCH 1/3] chore(jsx-email): update caniemail --- packages/jsx-email/package.json | 2 +- packages/jsx-email/src/cli/commands/check.ts | 24 ++-- .../test/cli/check-use-preview-props.test.ts | 2 +- pnpm-lock.yaml | 107 ++---------------- pnpm-workspace.yaml | 3 + 5 files changed, 33 insertions(+), 105 deletions(-) diff --git a/packages/jsx-email/package.json b/packages/jsx-email/package.json index 6421897a..0754978c 100644 --- a/packages/jsx-email/package.json +++ b/packages/jsx-email/package.json @@ -78,7 +78,7 @@ "@vitejs/plugin-react": "catalog:", "autoprefixer": "catalog:", "bwip-js": "^4.9.2", - "caniemail": "1.0.0", + "caniemail": "2.0.2", "chalk": "5.4.1", "chalk-template": "catalog:", "classnames": "2.5.1", diff --git a/packages/jsx-email/src/cli/commands/check.ts b/packages/jsx-email/src/cli/commands/check.ts index ff4ca07a..d7355dc4 100644 --- a/packages/jsx-email/src/cli/commands/check.ts +++ b/packages/jsx-email/src/cli/commands/check.ts @@ -1,6 +1,12 @@ import { lstat } from 'node:fs/promises'; -import { type IssueGroup, caniemail, groupIssues, sortIssues } from 'caniemail'; +import { + type CanIEmailOptions, + type IssueGroup, + caniemail, + groupIssues, + sortIssues +} from 'caniemail'; import chalk from 'chalk'; import chalkTmpl from 'chalk-template'; import stripAnsi from 'strip-ansi'; @@ -26,6 +32,7 @@ const CheckOptionsStruct = object({ }); type CheckOptions = Infer; +type EmailClients = CanIEmailOptions['clients']; const defaultEmailClients = [ 'apple-mail.*', @@ -34,7 +41,7 @@ const defaultEmailClients = [ 'protonmail.*', 'hey.*', 'fastmail.*' -]; +] satisfies EmailClients; export const help = chalkTmpl` {blue email check} @@ -81,10 +88,13 @@ const formatIssue = (group: IssueGroup): string => { return chalkTmpl`${preamble}${title}:${footnotes}\n${indent}{dim ${clients.join(`\n${indent}`)}}\n`; }; -const runCheck = (fileName: string, html: string, emailClients: string[]) => { +const parseEmailClients = (emailClients?: string): EmailClients => + emailClients ? (emailClients.split(',') as EmailClients) : [...defaultEmailClients]; + +const runCheck = (fileName: string, html: string, emailClients: EmailClients) => { const bytes = Buffer.byteLength(html, 'utf8'); const htmlSize = formatBytes(bytes); - const result = caniemail({ clients: emailClients as any, html }); + const result = caniemail({ clients: emailClients, html }); const { issues, success } = result; const { errors, warnings } = issues; const counts = { @@ -93,7 +103,7 @@ const runCheck = (fileName: string, html: string, emailClients: string[]) => { warnings: 0 }; - if (success && !issues.warnings) return; + if (success && !warnings.size) return; log(chalkTmpl`{underline ${fileName}} → HTML: ${htmlSize}\n`); @@ -105,7 +115,7 @@ const runCheck = (fileName: string, html: string, emailClients: string[]) => { counts.warnings += 1; } - if (errors?.size || warnings?.size) { + if (errors.size || warnings.size) { const groupedErrors = groupIssues(errors); const groupedWarnings = groupIssues(warnings); const sorted = sortIssues([...groupedErrors, ...groupedWarnings]); @@ -155,7 +165,7 @@ export const command: CommandFn = async (argv: CheckOptions, input) => { log(); - const emailClients = argv.emailClients ? argv.emailClients.split(',') : defaultEmailClients; + const emailClients = parseEmailClients(argv.emailClients); runCheck(file.fileName, file.html!, emailClients); diff --git a/packages/jsx-email/test/cli/check-use-preview-props.test.ts b/packages/jsx-email/test/cli/check-use-preview-props.test.ts index 1783f956..31420f3e 100644 --- a/packages/jsx-email/test/cli/check-use-preview-props.test.ts +++ b/packages/jsx-email/test/cli/check-use-preview-props.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import { previewProps } from './fixtures/cli-preview-props-template.js'; const caniemailMock = vi.fn(() => ({ - issues: { errors: new Map(), warnings: undefined }, + issues: { errors: new Map(), warnings: new Map() }, success: true })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06f4eef6..0a9ef22e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: specifier: ^4.9.2 version: 4.9.2 caniemail: - specifier: 1.0.0 - version: 1.0.0(@vue/compiler-sfc@3.3.4)(prettier@3.5.3) + specifier: 2.0.2 + version: 2.0.2(@vue/compiler-sfc@3.3.4)(prettier@3.5.3) chalk: specifier: 5.4.1 version: 5.4.1 @@ -1006,10 +1006,6 @@ packages: resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.1': - resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -1052,11 +1048,6 @@ packages: resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.1': - resolution: {integrity: sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} @@ -1078,10 +1069,6 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.1': - resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} @@ -2007,10 +1994,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -2018,16 +2001,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -4204,9 +4180,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniemail@1.0.0: - resolution: {integrity: sha512-C2vF3xE0wuBlvB8WSvjsRy7PwnKdPk8B3+qhXgqXp5bWAYgaiIcerLIFFv0siKAsGF+m/CvSnbjNAv2PMXAiNA==} - engines: {node: '>=20.19.0'} + caniemail@2.0.2: + resolution: {integrity: sha512-lvhn4RrF2Ae3PQA3HMOVGxFPqPAxfv8HWp+xUSqJPumbzFUzQGVgFG0nl8s0sjSoBd2PgmAqB5xRF0wcPbrVcA==} + engines: {node: '>=22.0.0'} caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -4405,10 +4381,6 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -5214,10 +5186,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5516,9 +5484,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inline-style-parser@0.2.3: - resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -7675,9 +7640,6 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - style-to-object@1.0.6: - resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==} - stylehacks@7.0.10: resolution: {integrity: sha512-sRJ7klmhe/Fl5woJcbJUa2qP1Ueffsl1CQI4ePvqXLkZmcIuAt09aP9uT/FOFPqXh9Rh8M5UkgEnwTdTKn/Aag==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -8901,14 +8863,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.27.1': - dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.2 @@ -8956,10 +8910,6 @@ snapshots: '@babel/template': 7.28.6 '@babel/types': 7.29.0 - '@babel/parser@7.27.1': - dependencies: - '@babel/types': 7.29.0 - '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -8980,18 +8930,6 @@ snapshots: '@babel/parser': 7.29.2 '@babel/types': 7.29.0 - '@babel/traverse@7.27.1': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -9609,12 +9547,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -9622,15 +9554,8 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -10699,9 +10624,9 @@ snapshots: '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.3.4)(prettier@3.5.3)': dependencies: - '@babel/generator': 7.27.1 - '@babel/parser': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 javascript-natural-sort: 0.7.1 lodash: 4.17.21 @@ -11824,19 +11749,19 @@ snapshots: camelcase@5.3.1: {} - caniemail@1.0.0(@vue/compiler-sfc@3.3.4)(prettier@3.5.3): + caniemail@2.0.2(@vue/compiler-sfc@3.3.4)(prettier@3.5.3): dependencies: '@adobe/css-tools': 4.4.2 '@trivago/prettier-plugin-sort-imports': 5.2.2(@vue/compiler-sfc@3.3.4)(prettier@3.5.3) binary-search: 1.3.6 - css-what: 6.1.0 + css-what: 6.2.2 domhandler: 5.0.3 dot-prop: 9.0.0 htmlparser2: 10.0.0 micromatch: 4.0.8 onetime: 7.0.0 split-lines: 3.0.0 - style-to-object: 1.0.6 + style-to-object: 1.0.14 transitivePeerDependencies: - '@vue/compiler-sfc' - prettier @@ -12038,8 +11963,6 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 - css-what@6.1.0: {} - css-what@6.2.2: {} cssesc@3.0.0: {} @@ -13098,8 +13021,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@11.12.0: {} - globals@14.0.0: {} globals@15.13.0: {} @@ -13604,8 +13525,6 @@ snapshots: ini@1.3.8: {} - inline-style-parser@0.2.3: {} - inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -16236,10 +16155,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - style-to-object@1.0.6: - dependencies: - inline-style-parser: 0.2.3 - stylehacks@7.0.10(postcss@8.5.14): dependencies: browserslist: 4.28.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8fa6e0dd..d6a1ff55 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,6 +44,9 @@ catalog: enablePrePostScripts: true linkWorkspacePackages: false minimumReleaseAge: 10080 # 7 days +minimumReleaseAgeExclude: + - caniemail +minimumReleaseAgeStrict: false onlyBuiltDependencies: - '@moonrepo/cli' - '@parcel/watcher' From 0654b8f5c7cf5932baac328e3a66a49126dc978f Mon Sep 17 00:00:00 2001 From: shellscape Date: Sun, 17 May 2026 19:54:09 -0400 Subject: [PATCH 2/3] feat: support email clients for check in config file --- apps/web/src/content/docs/core/config.mdx | 8 ++++ packages/jsx-email/src/cli/commands/check.ts | 5 ++- packages/jsx-email/src/config.ts | 8 ++++ .../test/cli/check-use-preview-props.test.ts | 37 ++++++++++++++++++- .../test/config/define-config.test.ts | 10 +++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/web/src/content/docs/core/config.mdx b/apps/web/src/content/docs/core/config.mdx index ad9988c4..b1519cc1 100644 --- a/apps/web/src/content/docs/core/config.mdx +++ b/apps/web/src/content/docs/core/config.mdx @@ -80,6 +80,14 @@ export const config = defineConfig(async () => { jsx-email configuration files support the following properties: +```ts +check?: { + emailClients?: string[] +} +``` + +_Optional_. Allows the configuration file to specify the default email clients used by the CLI's `check` command. The `--email-clients` flag takes precedence when provided. + ```ts esbuild?: { plugins?: Plugin[] diff --git a/packages/jsx-email/src/cli/commands/check.ts b/packages/jsx-email/src/cli/commands/check.ts index d7355dc4..565b5b67 100644 --- a/packages/jsx-email/src/cli/commands/check.ts +++ b/packages/jsx-email/src/cli/commands/check.ts @@ -19,6 +19,7 @@ import { string } from 'valibot'; +import { current } from '../../config.js'; import { formatBytes, gmailByteLimit, gmailBytesSafe } from '../helpers.js'; import { buildTemplates } from './build.js'; @@ -89,7 +90,9 @@ const formatIssue = (group: IssueGroup): string => { }; const parseEmailClients = (emailClients?: string): EmailClients => - emailClients ? (emailClients.split(',') as EmailClients) : [...defaultEmailClients]; + emailClients + ? (emailClients.split(',') as EmailClients) + : (current().check?.emailClients ?? [...defaultEmailClients]); const runCheck = (fileName: string, html: string, emailClients: EmailClients) => { const bytes = Buffer.byteLength(html, 'utf8'); diff --git a/packages/jsx-email/src/config.ts b/packages/jsx-email/src/config.ts index bbaef2be..c87c1e55 100644 --- a/packages/jsx-email/src/config.ts +++ b/packages/jsx-email/src/config.ts @@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url'; import type { MethodFactoryLevels } from '@dot/log'; import { AssertionError } from 'assert'; +import type { CanIEmailOptions } from 'caniemail'; import chalk from 'chalk-template'; import { lilconfig } from 'lilconfig'; @@ -11,7 +12,12 @@ import { getPluginLog, log } from './log.js'; import { type JsxEmailPlugin, type PluginInternal, pluginSymbol } from './plugins.js'; import type { ESBuildOptions, RenderOptions } from './types.js'; +export interface CheckOptions { + emailClients?: CanIEmailOptions['clients']; +} + export interface JsxEmailConfig { + check?: CheckOptions; esbuild?: ESBuildOptions; logLevel?: MethodFactoryLevels; plugins: JsxEmailPlugin[]; @@ -293,5 +299,7 @@ export const mergeConfig = async (a: Partial, b: Partial ({ @@ -18,6 +19,10 @@ vi.mock('caniemail', () => ({ const templatePath = resolve(__dirname, 'fixtures/cli-preview-props-template.tsx'); describe('cli check --use-preview-props', () => { + afterEach(() => { + setConfig(defaults); + }); + it('builds using template.previewProps when flag is set', async () => { (globalThis as any).__jsxEmailCliPreviewProps = undefined; caniemailMock.mockClear(); @@ -35,4 +40,34 @@ describe('cli check --use-preview-props', () => { const [{ html }] = caniemailMock.mock.calls[0] as [{ html: string }]; expect(html).toContain('batman'); }); + + it('uses check.emailClients from config when the flag is omitted', async () => { + (globalThis as any).__jsxEmailCliPreviewProps = undefined; + setConfig({ ...defaults, check: { emailClients: ['gmx.*'] } }); + caniemailMock.mockClear(); + + const { command } = await import('../../src/cli/commands/check.js'); + + const result = await command({ usePreviewProps: true } as any, [templatePath]); + + expect(result).toBe(true); + expect(caniemailMock).toHaveBeenCalledWith(expect.objectContaining({ clients: ['gmx.*'] })); + }); + + it('prefers --email-clients over check.emailClients from config', async () => { + setConfig({ ...defaults, check: { emailClients: ['gmx.*'] } }); + caniemailMock.mockClear(); + + const { command } = await import('../../src/cli/commands/check.js'); + + const result = await command( + { emailClients: 'gmail.*,outlook.*', usePreviewProps: true } as any, + [templatePath] + ); + + expect(result).toBe(true); + expect(caniemailMock).toHaveBeenCalledWith( + expect.objectContaining({ clients: ['gmail.*', 'outlook.*'] }) + ); + }); }); diff --git a/packages/jsx-email/test/config/define-config.test.ts b/packages/jsx-email/test/config/define-config.test.ts index 7974cf44..9ce2d2b8 100644 --- a/packages/jsx-email/test/config/define-config.test.ts +++ b/packages/jsx-email/test/config/define-config.test.ts @@ -15,6 +15,16 @@ describe('defineConfig', async () => { expect(config).toMatchSnapshot(); }); + test('check clients', async () => { + const config = await defineConfig({ + check: { + emailClients: ['gmail.*', 'outlook.*'] + } + }); + + expect(config.check?.emailClients).toEqual(['gmail.*', 'outlook.*']); + }); + test('minify and pretty', async () => { const config = await defineConfig({ render: { From e669d851920ac8be801d64edc6249f65095908aa Mon Sep 17 00:00:00 2001 From: shellscape Date: Sun, 17 May 2026 20:31:18 -0400 Subject: [PATCH 3/3] chore: update docs --- apps/web/src/content/docs/core/cli.mdx | 8 ++++++++ apps/web/src/content/docs/core/config.mdx | 20 +++++++++++++++++++- apps/web/src/content/docs/v3/migration.mdx | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/web/src/content/docs/core/cli.mdx b/apps/web/src/content/docs/core/cli.mdx index 8a0431ac..02ad3d9d 100644 --- a/apps/web/src/content/docs/core/cli.mdx +++ b/apps/web/src/content/docs/core/cli.mdx @@ -85,6 +85,14 @@ $ email check ./emails/Batman.tsx To check templates using their exported `previewProps`, pass the `--use-preview-props` flag so the check runs against the same sample data as your previews and builds. +To limit compatibility checks to specific email clients, pass `--email-clients` with a comma-separated list of caniemail client names or globs: + +```console +$ email check ./emails/Batman.tsx --email-clients gmail.*,outlook.* +``` + +Default check clients can also be configured in `jsx-email.config.*` with `check.emailClients`. The CLI flag takes precedence over config when both are present. + Example output: ```console diff --git a/apps/web/src/content/docs/core/config.mdx b/apps/web/src/content/docs/core/config.mdx index b1519cc1..a51662df 100644 --- a/apps/web/src/content/docs/core/config.mdx +++ b/apps/web/src/content/docs/core/config.mdx @@ -86,7 +86,25 @@ check?: { } ``` -_Optional_. Allows the configuration file to specify the default email clients used by the CLI's `check` command. The `--email-clients` flag takes precedence when provided. +_Optional_. Allows the configuration file to specify the default email clients used by the CLI's `check` command. + +```ts +import { defineConfig } from 'jsx-email/config'; + +export const config = defineConfig({ + check: { + emailClients: ['gmail.*', 'outlook.*'] + } +}); +``` + +The `emailClients` values use the same client names and glob patterns supported by [caniemail](https://www.caniemail.com/). Exact clients such as `gmail.desktop-webmail`, provider globs such as `outlook.*`, platform globs such as `*.ios`, and `*` for all clients are supported. + +The CLI's `--email-clients` flag takes precedence over this config value when provided: + +```console +$ email check ./emails/welcome.tsx --email-clients apple-mail.*,gmail.* +``` ```ts esbuild?: { diff --git a/apps/web/src/content/docs/v3/migration.mdx b/apps/web/src/content/docs/v3/migration.mdx index ba242219..7d2b8ab0 100644 --- a/apps/web/src/content/docs/v3/migration.mdx +++ b/apps/web/src/content/docs/v3/migration.mdx @@ -115,10 +115,10 @@ email check ./emails/welcome.tsx --use-preview-props `email check` can limit compatibility validation to a comma-separated list of email clients. ```sh -email check ./emails/welcome.tsx --email-clients gmail,outlook +email check ./emails/welcome.tsx --email-clients gmail.*,outlook.* ``` -The v3 check command remains caniemail-based. +The v3 check command remains caniemail-based. Default check clients can also be configured with `check.emailClients` in `jsx-email.config.*`. ## New Components