Skip to content

Commit 4eb0fe5

Browse files
felipefreitagReemXgabrielmfern
committed
feat(react-email): filter compatibility warnings by email client
Adds --clients to email dev and reads COMPATIBILITY_EMAIL_CLIENTS from the environment so teams that only target a subset of clients can quiet warnings for the rest. The CLI flag wins over the env var; an empty or fully-invalid list falls back to the defaults so warnings can't be silently switched off. The Compatibility tab shows the active client list above the results so the filter is never invisible. Builds on #2797 by @ReemX with the cubic-flagged regressions fixed: the port-retry path now preserves the flag, undefined no longer clobbers a user-set env var, and invalid env values fall back to the defaults instead of suppressing all checks. Co-authored-by: ReemX <reemasaf44@gmail.com> Co-authored-by: Gabriel Miranda <gabrielmfern@outlook.com>
1 parent c295594 commit 4eb0fe5

12 files changed

Lines changed: 148 additions & 36 deletions

File tree

.changeset/clients-compat-flag.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-email/ui": minor
3+
"react-email": minor
4+
---
5+
6+
add a `--clients` option to `email dev` and a `COMPATIBILITY_EMAIL_CLIENTS` environment variable to narrow which email clients trigger compatibility warnings. By default the preview still warns for `gmail`, `apple-mail`, `outlook`, and `yahoo`. Teams that only target one or two clients can now skip the noise: `email dev --clients outlook,apple-mail`. The CLI flag wins over the env var; an empty or fully-invalid list falls back to the defaults so warnings can't be silently switched off. Builds on #2797 by @ReemX.

packages/react-email/src/cli/commands/dev.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import { setupHotreloading, startDevServer } from '../utils/index.js';
44
interface Args {
55
dir: string;
66
port: string;
7+
clients?: string;
78
}
89

9-
export const dev = async ({ dir: emailsDirRelativePath, port }: Args) => {
10+
export const dev = async ({
11+
dir: emailsDirRelativePath,
12+
port,
13+
clients,
14+
}: Args) => {
1015
try {
1116
if (!fs.existsSync(emailsDirRelativePath)) {
1217
console.error(`Missing ${emailsDirRelativePath} folder`);
@@ -17,6 +22,7 @@ export const dev = async ({ dir: emailsDirRelativePath, port }: Args) => {
1722
emailsDirRelativePath,
1823
emailsDirRelativePath, // defaults to ./emails/static for the static files that are served to the preview
1924
Number.parseInt(port, 10),
25+
clients,
2026
);
2127

2228
await setupHotreloading(devServer, emailsDirRelativePath);

packages/react-email/src/cli/index.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
11
#!/usr/bin/env node
22
import { spawn } from 'node:child_process';
3-
import { Option, program } from 'commander';
3+
import { InvalidArgumentError, Option, program } from 'commander';
44
import { build } from './commands/build.js';
55
import { dev } from './commands/dev.js';
66
import { exportTemplates } from './commands/export.js';
77
import { resendReset } from './commands/resend/reset.js';
88
import { resendSetup } from './commands/resend/setup.js';
99
import { start } from './commands/start.js';
10+
import { ALL_EMAIL_CLIENTS } from './utils/email-clients.js';
1011
import { packageJson } from './utils/packageJson.js';
1112

13+
const parseClientsOption = (value: string): string => {
14+
const requested = value
15+
.split(',')
16+
.map((entry) => entry.trim().toLowerCase())
17+
.filter(Boolean);
18+
19+
if (requested.length === 0) {
20+
throw new InvalidArgumentError(
21+
'--clients requires at least one email client.',
22+
);
23+
}
24+
25+
const known = new Set<string>(ALL_EMAIL_CLIENTS);
26+
const invalid = requested.filter((entry) => !known.has(entry));
27+
if (invalid.length > 0) {
28+
throw new InvalidArgumentError(
29+
`Unknown email client(s): ${invalid.join(', ')}. Supported: ${ALL_EMAIL_CLIENTS.join(', ')}.`,
30+
);
31+
}
32+
33+
return requested.join(',');
34+
};
35+
1236
const requiredFlags = [
1337
'--experimental-vm-modules',
1438
'--disable-warning=ExperimentalWarning',
@@ -50,6 +74,11 @@ if (!hasRequiredFlags) {
5074
'./emails',
5175
)
5276
.option('-p --port <port>', 'Port to run dev server on', '3000')
77+
.option(
78+
'-c, --clients <clients>',
79+
'Comma-separated list of email clients to show compatibility warnings for (overrides COMPATIBILITY_EMAIL_CLIENTS)',
80+
parseClientsOption,
81+
)
5382
.action(dev);
5483

5584
program
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Kept in sync with ALL_EMAIL_CLIENTS in
2+
// packages/ui/src/actions/email-validation/check-compatibility.ts.
3+
// Duplicated here so the CLI can validate --clients without depending on
4+
// @react-email/ui at runtime.
5+
export const ALL_EMAIL_CLIENTS = [
6+
'gmail',
7+
'outlook',
8+
'yahoo',
9+
'apple-mail',
10+
'aol',
11+
'thunderbird',
12+
'microsoft',
13+
'samsung-email',
14+
'sfr',
15+
'orange',
16+
'protonmail',
17+
'hey',
18+
'mail-ru',
19+
'fastmail',
20+
'laposte',
21+
't-online-de',
22+
'free-fr',
23+
'gmx',
24+
'web-de',
25+
'ionos-1and1',
26+
'rainloop',
27+
'wp-pl',
28+
] as const;

packages/react-email/src/cli/utils/preview/get-env-variables-for-preview-app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const getEnvVariablesForPreviewApp = (
55
previewServerLocation: string,
66
cwd: string,
77
resendApiKey?: string,
8+
compatibilityClients?: string,
89
) => {
910
return {
1011
REACT_EMAIL_INTERNAL_EMAILS_DIR_RELATIVE_PATH:
@@ -16,5 +17,9 @@ export const getEnvVariablesForPreviewApp = (
1617
REACT_EMAIL_INTERNAL_PREVIEW_SERVER_LOCATION: previewServerLocation,
1718
REACT_EMAIL_INTERNAL_USER_PROJECT_LOCATION: cwd,
1819
REACT_EMAIL_INTERNAL_RESEND_API_KEY: resendApiKey,
20+
// Only spread the key when set so a user-provided env var isn't clobbered.
21+
...(compatibilityClients !== undefined && {
22+
COMPATIBILITY_EMAIL_CLIENTS: compatibilityClients,
23+
}),
1924
} as const;
2025
};

packages/react-email/src/cli/utils/preview/start-dev-server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const startDevServer = async (
3333
emailsDirRelativePath: string,
3434
staticBaseDirRelativePath: string,
3535
port: number,
36+
compatibilityClients?: string,
3637
): Promise<http.Server> => {
3738
const [majorNodeVersion] = process.versions.node.split('.');
3839
if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) {
@@ -100,6 +101,7 @@ export const startDevServer = async (
100101
emailsDirRelativePath,
101102
staticBaseDirRelativePath,
102103
nextPortToTry,
104+
compatibilityClients,
103105
);
104106
}
105107

@@ -137,6 +139,7 @@ export const startDevServer = async (
137139
previewServerLocation,
138140
process.cwd(),
139141
conf.get('resendApiKey'),
142+
compatibilityClients,
140143
),
141144
};
142145
if (!process.env.ESBUILD_BINARY_PATH) {

packages/react-email/src/cli/utils/tree.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test('tree(__dirname, 2)', async () => {
1717
│ ├── hot-reload-change.ts
1818
│ └── hot-reload-event.ts
1919
├── conf.ts
20+
├── email-clients.ts
2021
├── get-emails-directory-metadata.spec.ts
2122
├── get-emails-directory-metadata.ts
2223
├── get-ui-location.ts

packages/ui/src/actions/email-validation/check-compatibility.ts

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {
1111
doesPropertyHaveLocation,
1212
getUsedStyleProperties,
1313
} from '../../utils/caniemail/ast/get-used-style-properties';
14+
import {
15+
type EmailClient,
16+
getRelevantEmailClients,
17+
} from '../../utils/caniemail/email-clients';
1418
import type {
1519
CompatibilityStats,
1620
SupportStatus,
@@ -34,30 +38,6 @@ export interface CompatibilityCheckingResult {
3438
statsPerEmailClient: CompatibilityStats['perEmailClient'];
3539
}
3640

37-
export type EmailClient =
38-
| 'gmail'
39-
| 'outlook'
40-
| 'yahoo'
41-
| 'apple-mail'
42-
| 'aol'
43-
| 'thunderbird'
44-
| 'microsoft'
45-
| 'samsung-email'
46-
| 'sfr'
47-
| 'orange'
48-
| 'protonmail'
49-
| 'hey'
50-
| 'mail-ru'
51-
| 'fastmail'
52-
| 'laposte'
53-
| 't-online-de'
54-
| 'free-fr'
55-
| 'gmx'
56-
| 'web-de'
57-
| 'ionos-1and1'
58-
| 'rainloop'
59-
| 'wp-pl';
60-
6141
export type Platform =
6242
| 'desktop-app'
6343
| 'desktop-webmail'
@@ -123,17 +103,11 @@ export type SupportEntry =
123103
source: 'react-email';
124104
});
125105

126-
const relevantEmailClients: EmailClient[] = [
127-
'gmail',
128-
'apple-mail',
129-
'outlook',
130-
'yahoo',
131-
];
132-
133106
export const checkCompatibility = async (
134107
reactCode: string,
135108
emailPath: string,
136109
) => {
110+
const relevantEmailClients = getRelevantEmailClients();
137111
const ast = parse(reactCode, {
138112
strictMode: false,
139113
errorRecovery: true,

packages/ui/src/app/preview/[...slug]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { LintingRow } from '../../../components/toolbar/linter';
1313
import type { SpamCheckingResult } from '../../../components/toolbar/spam-assassin';
1414
import { PreviewProvider } from '../../../contexts/preview';
1515
import { ToolbarProvider } from '../../../contexts/toolbar';
16+
import { getRelevantEmailClients } from '../../../utils/caniemail/email-clients';
1617
import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
1718
import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
1819
import { loadStream } from '../../../utils/load-stream';
@@ -139,6 +140,7 @@ This is most likely not an issue with the preview server. Maybe there was a typo
139140
serverLintingRows={lintingRows}
140141
serverSpamCheckingResult={spamCheckingResult}
141142
serverCompatibilityResults={compatibilityCheckingResults}
143+
serverCompatibilityClients={getRelevantEmailClients()}
142144
/>
143145
</ToolbarProvider>
144146
</Suspense>

packages/ui/src/components/toolbar.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LayoutGroup } from 'framer-motion';
55
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
66
import type { ComponentProps } from 'react';
77
import * as React from 'react';
8+
import { nicenames } from '../actions/email-validation/caniemail-data';
89
import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility';
910
import { isBuilding } from '../app/env';
1011
import { usePreviewContext } from '../contexts/preview';
@@ -51,6 +52,7 @@ const ToolbarInner = ({
5152
serverLintingRows,
5253
serverSpamCheckingResult,
5354
serverCompatibilityResults,
55+
serverCompatibilityClients,
5456

5557
prettyMarkup,
5658
reactMarkup,
@@ -72,6 +74,10 @@ const ToolbarInner = ({
7274

7375
const { hasSetupResendIntegration } = useToolbarContext();
7476

77+
const compatibilityClientsLabel = serverCompatibilityClients
78+
.map((client) => nicenames.family[client] ?? client)
79+
.join(', ');
80+
7581
const { activeTab, toggled } = useToolbarState();
7682

7783
const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
@@ -283,7 +289,8 @@ const ToolbarInner = ({
283289
<SuccessIcon />
284290
<SuccessTitle>Great compatibility</SuccessTitle>
285291
<SuccessDescription>
286-
Template should render properly everywhere.
292+
Template should render properly in{' '}
293+
{compatibilityClientsLabel}.
287294
</SuccessDescription>
288295
</SuccessWrapper>
289296
) : (
@@ -406,12 +413,14 @@ interface ToolbarProps {
406413
serverSpamCheckingResult: SpamCheckingResult | undefined;
407414
serverLintingRows: LintingRow[] | undefined;
408415
serverCompatibilityResults: CompatibilityCheckingResult[] | undefined;
416+
serverCompatibilityClients: readonly string[];
409417
}
410418

411419
export function Toolbar({
412420
serverLintingRows,
413421
serverSpamCheckingResult,
414422
serverCompatibilityResults,
423+
serverCompatibilityClients,
415424
}: ToolbarProps) {
416425
const { emailPath, emailSlug, renderedEmailMetadata } = usePreviewContext();
417426

@@ -430,6 +439,7 @@ export function Toolbar({
430439
serverLintingRows={serverLintingRows}
431440
serverSpamCheckingResult={serverSpamCheckingResult}
432441
serverCompatibilityResults={serverCompatibilityResults}
442+
serverCompatibilityClients={serverCompatibilityClients}
433443
/>
434444
);
435445
}

0 commit comments

Comments
 (0)