diff --git a/packages/react-email/src/actions/email-validation/check-links.ts b/packages/react-email/src/actions/email-validation/check-links.ts index 9681721aac..f831867622 100644 --- a/packages/react-email/src/actions/email-validation/check-links.ts +++ b/packages/react-email/src/actions/email-validation/check-links.ts @@ -1,7 +1,10 @@ 'use server'; /* eslint-disable @typescript-eslint/no-non-null-assertion */ import http from 'node:http'; +import { promises as fs } from 'node:fs'; import traverse from '@babel/traverse'; +import { Passero_One } from 'next/font/google'; +import { getEmailPathFromSlug } from '../get-email-path-from-slug'; import { getLineAndColumnFromIndex } from './get-line-and-column-from-index'; import { parseCode } from './parse-code'; @@ -23,21 +26,22 @@ const doesURLSucceedResponse = async (url: URL) => { return succeedingURLs.get(url)!; }; -interface LinkCheck { - type: 'syntax' | 'security' | 'response-code'; - status: 'failed' | 'passed'; -} - -interface LinkCheckingResult { +export interface LinkCheckingResult { link: string; line: number; column: number; - checks: LinkCheck[]; + checks: { + syntax: 'failed' | 'passed'; + security?: 'failed' | 'passed'; + responseCode?: 'failed' | 'passed'; + }; } -export const checkLinks = async (code: string) => { +export const checkLinks = async (emailSlug: string) => { + const emailPath = await getEmailPathFromSlug(emailSlug); + const code = await fs.readFile(emailPath, 'utf8'); const ast = parseCode(code); const linkCheckingResults: LinkCheckingResult[] = []; @@ -49,11 +53,10 @@ export const checkLinks = async (code: string) => { }[] = []; traverse(ast, { - // JSXOpeningElement(nodePath) { if ( nodePath.node.name.type !== 'JSXIdentifier' || - nodePath.node.name.name !== 'a' + (nodePath.node.name.name !== 'a' && nodePath.node.name.name !== 'Link') ) return; @@ -95,34 +98,19 @@ export const checkLinks = async (code: string) => { continue; } - if (link.startsWith('/')) { - continue; - } - - const checks: LinkCheck[] = []; + const checks: LinkCheckingResult['checks'] = { + syntax: 'passed', + }; try { const url = new URL(link); - checks.push({ - type: 'syntax', - status: 'passed', - }); const hasSucceeded = await doesURLSucceedResponse(url); - checks.push({ - type: 'response-code', - status: hasSucceeded ? 'passed' : 'failed', - }); + checks.responseCode = hasSucceeded ? 'passed' : 'failed'; - checks.push({ - type: 'security', - status: link.startsWith('https://') ? 'passed' : 'failed', - }); + checks.security = link.startsWith('https://') ? 'passed' : 'failed'; } catch (exception) { - checks.push({ - type: 'syntax', - status: 'failed', - }); + checks.syntax = 'failed'; } linkCheckingResults.push({ diff --git a/packages/react-email/src/components/sidebar/link-checker.tsx b/packages/react-email/src/components/sidebar/link-checker.tsx new file mode 100644 index 0000000000..cb78d228bd --- /dev/null +++ b/packages/react-email/src/components/sidebar/link-checker.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import clsx from 'clsx'; +import { + checkLinks, + type LinkCheckingResult, +} from '../../actions/email-validation/check-links'; +import { Button } from '../button'; + +const checkingResultsCache = new Map(); + +interface LinkCheckerProps { + currentEmailOpenSlug: string; +} + +export const LinkChecker = ({ currentEmailOpenSlug }: LinkCheckerProps) => { + const [results, setResults] = React.useState< + LinkCheckingResult[] | undefined + >(checkingResultsCache.get(currentEmailOpenSlug)); + + const handleRun = () => { + checkLinks(currentEmailOpenSlug) + .then((newResults) => { + checkingResultsCache.set(currentEmailOpenSlug, newResults); + setResults(newResults); + }) + .catch((exception) => { + throw exception; + }); + }; + + console.log(results); + + return ( +
+

Link Checker

+ {results ? ( + <> +
    + {results.map((result) => ( + + ))} +
+ + + ) : ( + <> + + Check if all links are valid and going to the right pages + + + + )} +
+ ); +}; + +const LinkCheckingResultView = (props: LinkCheckingResult) => { + let status: 'success' | 'error' | 'warning' = 'success'; + if (props.checks.security === 'failed') { + status = 'warning'; + } + if (props.checks.syntax === 'failed') { + status = 'error'; + } + if (props.checks.responseCode === 'failed') { + status = 'error'; + } + return ( +
  • +
    + {props.link} +
    +
  • + ); +}; diff --git a/packages/react-email/src/components/sidebar/sidebar.tsx b/packages/react-email/src/components/sidebar/sidebar.tsx index 953a0c97a0..0636cf4ce6 100644 --- a/packages/react-email/src/components/sidebar/sidebar.tsx +++ b/packages/react-email/src/components/sidebar/sidebar.tsx @@ -10,6 +10,7 @@ import { Logo } from '../logo'; import { IconEmail } from '../icons/icon-email'; import { IconLink } from '../icons/icon-link'; import { SidebarDirectoryChildren } from './sidebar-directory-children'; +import { LinkChecker } from './link-checker'; interface SidebarProps { className?: string; @@ -21,25 +22,28 @@ interface SidebarTabTriggerProps { className?: string; children?: React.ReactNode; + disabled?: boolean; tabValue: SidebarTab; activeTabValue: SidebarTab; } -type SidebarTab = 'email-templates' | 'link-checker'; +type SidebarTab = 'file-tree' | 'link-checker'; const SidebarTabTrigger = ({ children, className, tabValue, + disabled, activeTabValue, }: SidebarTabTriggerProps) => { return ( {children} @@ -53,7 +57,7 @@ export const Sidebar = ({ style, }: SidebarProps) => { const [activeTabValue, setActiveTabValue] = - React.useState('email-templates'); + React.useState('file-tree'); const { emailsDirectoryMetadata } = useEmails(); return ( @@ -69,17 +73,18 @@ export const Sidebar = ({ className={cn('border-r flex flex-col border-slate-6', className)} style={{ ...style }} > -
    +
    @@ -89,25 +94,27 @@ export const Sidebar = ({
    - {/*activeTabValue === 'link-checker' && emailValidationWarnings ? ( - - ) : null*/} - {activeTabValue === 'email-templates' ? ( -
    - -
    - ) : null} +
    + {activeTabValue === 'link-checker' && currentEmailOpenSlug ? ( + + ) : null} + {activeTabValue === 'file-tree' ? ( +
    + +
    + ) : null} +