Skip to content

Commit 19f4660

Browse files
authored
feat(react-email): Add line/columns for image and link checkers (#1963)
1 parent 5c7fa0a commit 19f4660

14 files changed

+149
-137
lines changed

apps/demo/emails/magic-links/aws-verify-email.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default function AWSVerifyEmail({
8484
}
8585

8686
AWSVerifyEmail.PreviewProps = {
87-
verificationCode: '596853'
87+
verificationCode: '596853',
8888
} satisfies AWSVerifyEmailProps;
8989

9090
const main = {

packages/react-email/src/actions/email-validation/check-images.spec.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import { render } from '@react-email/render';
21
import { type ImageCheckingResult, checkImages } from './check-images';
32

43
test('checkImages()', async () => {
54
const results: ImageCheckingResult[] = [];
6-
const stream = await checkImages(
7-
await render(
8-
<div>
9-
{/* biome-ignore lint/a11y/useAltText: This is intentional to test the checking for accessibility */}
10-
<img src="https://resend.com/static/brand/resend-icon-white.png" />,
11-
<img src="/static/codepen-challengers.png" alt="codepen challenges" />,
12-
</div>,
13-
),
14-
'https://demo.react.email',
15-
);
5+
const html = `<div>
6+
<img src="https://resend.com/static/brand/resend-icon-white.png" />,
7+
<img src="/static/codepen-challengers.png" alt="codepen challenges" />,
8+
</div>`;
9+
const stream = await checkImages(html, 'https://demo.react.email');
1610
const reader = stream.getReader();
1711
while (true) {
1812
const { done, value } = await reader.read();
@@ -26,6 +20,10 @@ test('checkImages()', async () => {
2620
expect(results).toEqual([
2721
{
2822
source: 'https://resend.com/static/brand/resend-icon-white.png',
23+
codeLocation: {
24+
line: 2,
25+
column: 3,
26+
},
2927
checks: [
3028
{
3129
passed: false,
@@ -60,6 +58,10 @@ test('checkImages()', async () => {
6058
status: 'warning',
6159
},
6260
{
61+
codeLocation: {
62+
line: 3,
63+
column: 3,
64+
},
6365
checks: [
6466
{
6567
metadata: {

packages/react-email/src/actions/email-validation/check-images.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import type { IncomingMessage } from 'node:http';
44
import { parse } from 'node-html-parser';
5+
import {
6+
type CodeLocation,
7+
getCodeLocationFromAstElement,
8+
} from './get-code-location-from-ast-element';
59
import { quickFetch } from './quick-fetch';
610

711
export type ImageCheck = { passed: boolean } & (
@@ -34,6 +38,7 @@ export type ImageCheck = { passed: boolean } & (
3438
export interface ImageCheckingResult {
3539
status: 'success' | 'warning' | 'error';
3640
source: string;
41+
codeLocation: CodeLocation;
3742
checks: ImageCheck[];
3843
}
3944

@@ -61,6 +66,7 @@ export const checkImages = async (code: string, base: string) => {
6166

6267
const result: ImageCheckingResult = {
6368
source: rawSource,
69+
codeLocation: getCodeLocationFromAstElement(image, code),
6470
status: 'success',
6571
checks: [],
6672
};

packages/react-email/src/actions/email-validation/check-links.spec.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import { render } from '@react-email/render';
21
import { type LinkCheckingResult, checkLinks } from './check-links';
32

43
test('checkLinks()', async () => {
54
const results: LinkCheckingResult[] = [];
6-
const stream = await checkLinks(
7-
await render(
8-
<div>
9-
<a href="/">Root</a>
10-
<a href="https://resend.com">Resend</a>
11-
<a href="https://notion.so">Notion</a>
12-
<a href="http://react.email">React Email unsafe</a>
13-
</div>,
14-
),
15-
);
5+
const html = `<div>
6+
<a href="/">Root</a>
7+
<a href="https://resend.com">Resend</a>
8+
<a href="https://notion.so">Notion</a>
9+
<a href="http://react.email">React Email unsafe</a>
10+
</div>`;
11+
const stream = await checkLinks(html);
1612
const reader = stream.getReader();
1713
while (true) {
1814
const { done, value } = await reader.read();
@@ -26,6 +22,10 @@ test('checkLinks()', async () => {
2622
expect(results).toEqual([
2723
{
2824
status: 'error',
25+
codeLocation: {
26+
line: 2,
27+
column: 3,
28+
},
2929
checks: [
3030
{
3131
type: 'syntax',
@@ -36,6 +36,10 @@ test('checkLinks()', async () => {
3636
},
3737
{
3838
status: 'success',
39+
codeLocation: {
40+
line: 3,
41+
column: 3,
42+
},
3943
checks: [
4044
{
4145
type: 'syntax',
@@ -57,6 +61,10 @@ test('checkLinks()', async () => {
5761
},
5862
{
5963
status: 'warning',
64+
codeLocation: {
65+
line: 4,
66+
column: 3,
67+
},
6068
checks: [
6169
{
6270
type: 'syntax',
@@ -78,6 +86,10 @@ test('checkLinks()', async () => {
7886
},
7987
{
8088
status: 'warning',
89+
codeLocation: {
90+
line: 5,
91+
column: 3,
92+
},
8193
checks: [
8294
{
8395
type: 'syntax',

packages/react-email/src/actions/email-validation/check-links.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use server';
22

33
import { parse } from 'node-html-parser';
4+
import {
5+
type CodeLocation,
6+
getCodeLocationFromAstElement,
7+
} from './get-code-location-from-ast-element';
48
import { quickFetch } from './quick-fetch';
59

610
export type LinkCheck = { passed: boolean } & (
@@ -21,6 +25,7 @@ export type LinkCheck = { passed: boolean } & (
2125
export interface LinkCheckingResult {
2226
status: 'success' | 'warning' | 'error';
2327
link: string;
28+
codeLocation: CodeLocation;
2429
checks: LinkCheck[];
2530
}
2631

@@ -37,6 +42,7 @@ export const checkLinks = async (code: string) => {
3742

3843
const result: LinkCheckingResult = {
3944
link,
45+
codeLocation: getCodeLocationFromAstElement(anchor, code),
4046
status: 'success',
4147
checks: [],
4248
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { HTMLElement } from 'node-html-parser';
2+
import { getLineAndColumnFromOffset } from '../../utils/get-line-and-column-from-offset';
3+
4+
export interface CodeLocation {
5+
line: number;
6+
column: number;
7+
}
8+
9+
export const getCodeLocationFromAstElement = (
10+
ast: HTMLElement,
11+
html: string,
12+
): CodeLocation => {
13+
const [line, column] = getLineAndColumnFromOffset(ast.range[0], html);
14+
return {
15+
line,
16+
column,
17+
};
18+
};

packages/react-email/src/actions/email-validation/get-line-and-column-from-index.spec.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/react-email/src/actions/email-validation/get-line-and-column-from-index.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

packages/react-email/src/app/env.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export const emailsDirectoryAbsolutePath =
1111

1212
export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
1313

14-
export const isPreviewDevelopment = process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true';
14+
export const isPreviewDevelopment =
15+
process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ export const getEnvVariablesForPreviewApp = (
99
EMAILS_DIR_RELATIVE_PATH: relativePathToEmailsDirectory,
1010
EMAILS_DIR_ABSOLUTE_PATH: path.resolve(cwd, relativePathToEmailsDirectory),
1111
USER_PROJECT_LOCATION: cwd,
12-
NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT: isDev ? 'true': 'false',
12+
NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT: isDev ? 'true' : 'false',
1313
} as const;
1414
};

0 commit comments

Comments
 (0)