react-email 4.0.7 → 4.1.0-canary.2
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/CHANGELOG.md +19 -0
- package/dist/cli/index.js +20 -18
- package/dist/cli/index.mjs +20 -18
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +9 -9
- package/dist/preview/.next/app-path-routes-manifest.json +1 -1
- package/dist/preview/.next/build-manifest.json +3 -3
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +3 -3
- package/dist/preview/.next/required-server-files.json +4 -4
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +74 -72
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app-paths-manifest.json +1 -1
- package/dist/preview/.next/server/chunks/18.js +1 -0
- package/dist/preview/.next/server/chunks/840.js +2 -2
- package/dist/preview/.next/server/chunks/886.js +4 -4
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/chunks/{587-352f8079202a48d0.js → 587-4858c761db7745c7.js} +1 -1
- package/dist/preview/.next/static/chunks/app/layout-e9997533099ea6ce.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-dd13899a1b8e35f9.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-d668e90cc7af328b.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-a6a05ec7ce09e366.js → main-app-2cfbaf0185a1cd0e.js} +1 -1
- package/dist/preview/.next/trace +27 -27
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/page.ts +1 -1
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +16 -16
- package/src/actions/render-email-by-path.tsx +4 -3
- package/src/app/preview/[...slug]/preview.tsx +19 -1
- package/src/components/icons/icon-moon.tsx +16 -0
- package/src/components/icons/icon-sun.tsx +16 -0
- package/src/components/topbar/theme-toggle-group.tsx +87 -0
- package/src/hooks/use-email-rendering-result.ts +10 -1
- package/src/hooks/use-iframe-color-scheme.ts +35 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/src/utils/contains-email-template.spec.ts +86 -0
- package/src/utils/contains-email-template.ts +23 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +1 -1
- package/src/utils/get-email-component.ts +20 -1
- package/src/utils/get-emails-directory-metadata.ts +1 -1
- package/dist/preview/.next/server/chunks/380.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-fa93a7ef0cc5ebdb.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-0ee3a37f3a3f6f17.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-95449af2d870e732.js +0 -1
- /package/dist/preview/.next/static/{Oy7kpIZ6Nbnd7hpoEKBWw → SSj2mIeUmYssEdHvw7yXK}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{Oy7kpIZ6Nbnd7hpoEKBWw → SSj2mIeUmYssEdHvw7yXK}/_ssgManifest.js +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: /home/
|
|
1
|
+
// File: /home/runner/actions-runner/_work/react-email/react-email/packages/react-email/src/app/layout.tsx
|
|
2
2
|
import * as entry from '../../../src/app/layout.js'
|
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: /home/
|
|
1
|
+
// File: /home/runner/actions-runner/_work/react-email/react-email/packages/react-email/src/app/page.tsx
|
|
2
2
|
import * as entry from '../../../src/app/page.js'
|
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: /home/
|
|
1
|
+
// File: /home/runner/actions-runner/_work/react-email/react-email/packages/react-email/src/app/preview/[...slug]/page.tsx
|
|
2
2
|
import * as entry from '../../../../../src/app/preview/[...slug]/page.js'
|
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.1.0-canary.2",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"email": "./dist/cli/index.js"
|
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
"node": ">=18.0.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@babel/parser": "7.24.5",
|
|
23
|
-
"@babel/traverse": "7.25.6",
|
|
24
|
-
"chalk": "4.1.2",
|
|
25
|
-
"chokidar": "4.0.3",
|
|
26
|
-
"commander": "11.1.0",
|
|
27
|
-
"debounce": "2.0.0",
|
|
28
|
-
"esbuild": "0.25.0",
|
|
29
|
-
"glob": "10.3.4",
|
|
30
|
-
"log-symbols": "4.1.0",
|
|
31
|
-
"mime-types": "2.1.35",
|
|
32
|
-
"next": "15.2.4",
|
|
33
|
-
"normalize-path": "3.0.0",
|
|
34
|
-
"ora": "5.4.1",
|
|
35
|
-
"socket.io": "4.8.1"
|
|
22
|
+
"@babel/parser": "^7.24.5",
|
|
23
|
+
"@babel/traverse": "^7.25.6",
|
|
24
|
+
"chalk": "^4.1.2",
|
|
25
|
+
"chokidar": "^4.0.3",
|
|
26
|
+
"commander": "^11.1.0",
|
|
27
|
+
"debounce": "^2.0.0",
|
|
28
|
+
"esbuild": "^0.25.0",
|
|
29
|
+
"glob": "^10.3.4",
|
|
30
|
+
"log-symbols": "^4.1.0",
|
|
31
|
+
"mime-types": "^2.1.35",
|
|
32
|
+
"next": "^15.2.4",
|
|
33
|
+
"normalize-path": "^3.0.0",
|
|
34
|
+
"ora": "^5.4.1",
|
|
35
|
+
"socket.io": "^4.8.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@babel/core": "7.26.10",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"typescript": "5.8.2",
|
|
83
83
|
"use-debounce": "10.0.4",
|
|
84
84
|
"zod": "3.24.2",
|
|
85
|
-
"@react-email/components": "0.0.
|
|
85
|
+
"@react-email/components": "0.0.37-canary.2"
|
|
86
86
|
},
|
|
87
87
|
"scripts": {
|
|
88
88
|
"build": "tsup-node && node ./scripts/build-preview-server.mjs && pnpm install --frozen-lockfile",
|
|
@@ -58,15 +58,16 @@ export const renderEmailByPath = async (
|
|
|
58
58
|
emailComponent: Email,
|
|
59
59
|
createElement,
|
|
60
60
|
render,
|
|
61
|
+
pretty,
|
|
61
62
|
sourceMapToOriginalFile,
|
|
62
63
|
} = componentResult;
|
|
63
64
|
|
|
64
65
|
const previewProps = Email.PreviewProps || {};
|
|
65
66
|
const EmailComponent = Email as React.FC;
|
|
66
67
|
try {
|
|
67
|
-
const markup = await
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const markup = await pretty(
|
|
69
|
+
await render(createElement(EmailComponent, previewProps)),
|
|
70
|
+
);
|
|
70
71
|
const plainText = await render(
|
|
71
72
|
createElement(EmailComponent, previewProps),
|
|
72
73
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { use, useState } from 'react';
|
|
4
|
+
import { use, useRef, useState } from 'react';
|
|
5
5
|
import { flushSync } from 'react-dom';
|
|
6
6
|
import { Toaster } from 'sonner';
|
|
7
7
|
import { useDebouncedCallback } from 'use-debounce';
|
|
@@ -15,9 +15,11 @@ import { Send } from '../../../components/send';
|
|
|
15
15
|
import { useToolbarState } from '../../../components/toolbar';
|
|
16
16
|
import { Tooltip } from '../../../components/tooltip';
|
|
17
17
|
import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
|
|
18
|
+
import { ThemeToggleGroup } from '../../../components/topbar/theme-toggle-group';
|
|
18
19
|
import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
|
|
19
20
|
import { PreviewContext } from '../../../contexts/preview';
|
|
20
21
|
import { useClampedState } from '../../../hooks/use-clamped-state';
|
|
22
|
+
import { useIframeColorScheme } from '../../../hooks/use-iframe-color-scheme';
|
|
21
23
|
import { cn } from '../../../utils';
|
|
22
24
|
import { RenderingError } from './rendering-error';
|
|
23
25
|
|
|
@@ -32,9 +34,17 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
32
34
|
const pathname = usePathname();
|
|
33
35
|
const searchParams = useSearchParams();
|
|
34
36
|
|
|
37
|
+
const activeTheme: 'dark' | 'light' =
|
|
38
|
+
searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
|
35
39
|
const activeView = searchParams.get('view') ?? 'preview';
|
|
36
40
|
const activeLang = searchParams.get('lang') ?? 'jsx';
|
|
37
41
|
|
|
42
|
+
const handleThemeChange = (theme: 'dark' | 'light') => {
|
|
43
|
+
const params = new URLSearchParams(searchParams);
|
|
44
|
+
params.set('theme', theme);
|
|
45
|
+
router.push(`${pathname}?${params.toString()}`);
|
|
46
|
+
};
|
|
47
|
+
|
|
38
48
|
const handleViewChange = (view: string) => {
|
|
39
49
|
const params = new URLSearchParams(searchParams);
|
|
40
50
|
params.set('view', view);
|
|
@@ -51,6 +61,9 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
51
61
|
);
|
|
52
62
|
};
|
|
53
63
|
|
|
64
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
65
|
+
useIframeColorScheme(iframeRef, activeTheme);
|
|
66
|
+
|
|
54
67
|
const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
|
|
55
68
|
const hasErrors = 'error' in renderingResult;
|
|
56
69
|
|
|
@@ -99,6 +112,10 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
99
112
|
viewHeight={height}
|
|
100
113
|
viewWidth={width}
|
|
101
114
|
/>
|
|
115
|
+
<ThemeToggleGroup
|
|
116
|
+
active={activeTheme}
|
|
117
|
+
onChange={(theme) => handleThemeChange(theme)}
|
|
118
|
+
/>
|
|
102
119
|
<ActiveViewToggleGroup
|
|
103
120
|
activeView={activeView}
|
|
104
121
|
setActiveView={handleViewChange}
|
|
@@ -164,6 +181,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
164
181
|
<iframe
|
|
165
182
|
className="solid max-h-full rounded-lg bg-white"
|
|
166
183
|
ref={(iframe) => {
|
|
184
|
+
iframeRef.current = iframe;
|
|
167
185
|
if (iframe) {
|
|
168
186
|
return makeIframeDocumentBubbleEvents(iframe);
|
|
169
187
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconMoon = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
6
|
+
({ ...props }, forwardedRef) => (
|
|
7
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
8
|
+
<path
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
d="m17.75 4.09l-2.53 1.94l.91 3.06l-2.63-1.81l-2.63 1.81l.91-3.06l-2.53-1.94L12.44 4l1.06-3l1.06 3zm3.5 6.91l-1.64 1.25l.59 1.98l-1.7-1.17l-1.7 1.17l.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85c-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14c.4-.4.82-.76 1.27-1.08c.75-.53 1.93.36 1.85 1.19c-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82c-2.81 3.14-2.7 7.96.31 10.98c3.02 3.01 7.84 3.12 10.98.31"
|
|
11
|
+
/>
|
|
12
|
+
</IconBase>
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
IconMoon.displayName = 'IconMoon';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconSun = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
6
|
+
({ ...props }, forwardedRef) => (
|
|
7
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
8
|
+
<path
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
d="m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2"
|
|
11
|
+
/>
|
|
12
|
+
</IconBase>
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
IconSun.displayName = 'IconSun';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
import { tabTransition } from '../../utils/constants';
|
|
5
|
+
import { IconMoon } from '../icons/icon-moon';
|
|
6
|
+
import { IconSun } from '../icons/icon-sun';
|
|
7
|
+
import { Tooltip } from '../tooltip';
|
|
8
|
+
|
|
9
|
+
interface ThemeToggleGroupProps {
|
|
10
|
+
active: 'light' | 'dark';
|
|
11
|
+
onChange: (theme: 'light' | 'dark') => unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ThemeToggleGroup = ({
|
|
15
|
+
active,
|
|
16
|
+
onChange,
|
|
17
|
+
}: ThemeToggleGroupProps) => {
|
|
18
|
+
return (
|
|
19
|
+
<ToggleGroup.Root
|
|
20
|
+
aria-label="Color Scheme"
|
|
21
|
+
className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
|
|
22
|
+
id="theme-toggle"
|
|
23
|
+
onValueChange={(value) => {
|
|
24
|
+
if (value) onChange(value as 'light' | 'dark');
|
|
25
|
+
}}
|
|
26
|
+
type="single"
|
|
27
|
+
value={active}
|
|
28
|
+
>
|
|
29
|
+
<ToggleGroup.Item value="light">
|
|
30
|
+
<Tooltip>
|
|
31
|
+
<Tooltip.Trigger asChild>
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
|
|
35
|
+
{
|
|
36
|
+
'text-slate-11': active !== 'light',
|
|
37
|
+
'text-slate-12': active === 'light',
|
|
38
|
+
},
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{active === 'light' && (
|
|
42
|
+
<motion.span
|
|
43
|
+
animate={{ opacity: 1 }}
|
|
44
|
+
className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
|
|
45
|
+
exit={{ opacity: 0 }}
|
|
46
|
+
initial={{ opacity: 0 }}
|
|
47
|
+
layoutId="topbar-theme-tabs"
|
|
48
|
+
transition={tabTransition}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
<IconSun />
|
|
52
|
+
</div>
|
|
53
|
+
</Tooltip.Trigger>
|
|
54
|
+
<Tooltip.Content>Light</Tooltip.Content>
|
|
55
|
+
</Tooltip>
|
|
56
|
+
</ToggleGroup.Item>
|
|
57
|
+
<ToggleGroup.Item value="dark">
|
|
58
|
+
<Tooltip>
|
|
59
|
+
<Tooltip.Trigger asChild>
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
|
|
63
|
+
{
|
|
64
|
+
'text-slate-11': active !== 'dark',
|
|
65
|
+
'text-slate-12': active === 'dark',
|
|
66
|
+
},
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{active === 'dark' && (
|
|
70
|
+
<motion.span
|
|
71
|
+
animate={{ opacity: 1 }}
|
|
72
|
+
className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
|
|
73
|
+
exit={{ opacity: 0 }}
|
|
74
|
+
initial={{ opacity: 0 }}
|
|
75
|
+
layoutId="topbar-theme-tabs"
|
|
76
|
+
transition={tabTransition}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
<IconMoon />
|
|
80
|
+
</div>
|
|
81
|
+
</Tooltip.Trigger>
|
|
82
|
+
<Tooltip.Content>Dark</Tooltip.Content>
|
|
83
|
+
</Tooltip>
|
|
84
|
+
</ToggleGroup.Item>
|
|
85
|
+
</ToggleGroup.Root>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
renderEmailByPath,
|
|
6
6
|
} from '../actions/render-email-by-path';
|
|
7
7
|
import { isBuilding } from '../app/env';
|
|
8
|
+
import { useEmails } from '../contexts/emails';
|
|
9
|
+
import { containsEmailTemplate } from '../utils/contains-email-template';
|
|
8
10
|
import { useHotreload } from './use-hot-reload';
|
|
9
11
|
|
|
10
12
|
export const useEmailRenderingResult = (
|
|
@@ -15,6 +17,8 @@ export const useEmailRenderingResult = (
|
|
|
15
17
|
serverEmailRenderedResult,
|
|
16
18
|
);
|
|
17
19
|
|
|
20
|
+
const { emailsDirectoryMetadata } = useEmails();
|
|
21
|
+
|
|
18
22
|
if (!isBuilding) {
|
|
19
23
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
20
24
|
useHotreload(async (changes) => {
|
|
@@ -25,10 +29,15 @@ export const useEmailRenderingResult = (
|
|
|
25
29
|
// going to be equivalent to the slug
|
|
26
30
|
change.filename;
|
|
27
31
|
|
|
32
|
+
if (
|
|
33
|
+
containsEmailTemplate(slugForChangedEmail, emailsDirectoryMetadata)
|
|
34
|
+
) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
const pathForChangedEmail =
|
|
29
39
|
await getEmailPathFromSlug(slugForChangedEmail);
|
|
30
40
|
|
|
31
|
-
// We always render the email template here so that we can allow
|
|
32
41
|
const newRenderingResult = await renderEmailByPath(
|
|
33
42
|
pathForChangedEmail,
|
|
34
43
|
true,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export function useIframeColorScheme(
|
|
4
|
+
iframeRef: React.RefObject<HTMLIFrameElement | null>,
|
|
5
|
+
theme: string,
|
|
6
|
+
) {
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
const iframe = iframeRef.current;
|
|
9
|
+
|
|
10
|
+
if (!iframe) return;
|
|
11
|
+
|
|
12
|
+
// Set on iframe element itself
|
|
13
|
+
iframe.style.colorScheme = theme;
|
|
14
|
+
|
|
15
|
+
// Set on iframe's document if available
|
|
16
|
+
if (iframe.contentDocument) {
|
|
17
|
+
iframe.contentDocument.documentElement.style.colorScheme = theme;
|
|
18
|
+
iframe.contentDocument.body.style.colorScheme = theme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Ensure styles are applied after it loads
|
|
22
|
+
const handleLoad = () => {
|
|
23
|
+
if (iframe.contentDocument) {
|
|
24
|
+
iframe.contentDocument.documentElement.style.colorScheme = theme;
|
|
25
|
+
iframe.contentDocument.body.style.colorScheme = theme;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
iframe.addEventListener('load', handleLoad);
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
iframe.removeEventListener('load', handleLoad);
|
|
33
|
+
};
|
|
34
|
+
}, [theme, iframeRef]);
|
|
35
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="
|
|
3
|
+
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="margin-left:auto;margin-right:auto;margin-top:auto;margin-bottom:auto;background-color:rgb(255,255,255);padding-left:0.5rem;padding-right:0.5rem;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji""><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel Logo" height="37" src="/static/vercel-logo.png" style="margin-left:auto;margin-right:auto;margin-top:0px;margin-bottom:0px;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="margin-left:0px;margin-right:0px;margin-top:30px;margin-bottom:30px;padding:0px;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="Arrow indicating invitation" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img alt="Enigma team logo" height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px;margin-bottom:32px;text-align:center"><tbody><tr><td><a href="https://vercel.com" style="border-radius:0.25rem;background-color:rgb(0,0,0);padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;text-align:center;font-weight:600;font-size:12px;color:rgb(255,255,255);text-decoration-line:none;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>  ​</i><![endif]--></span></a></td></tr></tbody></table><p style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com</a></p><hr style="margin-left:0px;margin-right:0px;margin-top:26px;margin-bottom:26px;width:100%;border-width:1px;border-color:rgb(234,234,234);border-style:solid;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { containsEmailTemplate } from './contains-email-template';
|
|
3
|
+
|
|
4
|
+
test('containsEmailTemplate()', async () => {
|
|
5
|
+
const emailsDirectoryPath = path.resolve(
|
|
6
|
+
__dirname,
|
|
7
|
+
'../../../../apps/demo/emails',
|
|
8
|
+
);
|
|
9
|
+
const directory = {
|
|
10
|
+
absolutePath: emailsDirectoryPath,
|
|
11
|
+
directoryName: 'emails',
|
|
12
|
+
relativePath: '',
|
|
13
|
+
emailFilenames: [],
|
|
14
|
+
subDirectories: [
|
|
15
|
+
{
|
|
16
|
+
absolutePath: `${emailsDirectoryPath}/magic-links`,
|
|
17
|
+
directoryName: 'magic-links',
|
|
18
|
+
relativePath: 'magic-links',
|
|
19
|
+
emailFilenames: [
|
|
20
|
+
'aws-verify-email',
|
|
21
|
+
'linear-login-code',
|
|
22
|
+
'notion-magic-link',
|
|
23
|
+
'plaid-verify-identity',
|
|
24
|
+
'raycast-magic-link',
|
|
25
|
+
'slack-confirm',
|
|
26
|
+
],
|
|
27
|
+
subDirectories: [],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
absolutePath: `${emailsDirectoryPath}/newsletters`,
|
|
31
|
+
directoryName: 'newsletters',
|
|
32
|
+
relativePath: 'newsletters',
|
|
33
|
+
emailFilenames: [
|
|
34
|
+
'codepen-challengers',
|
|
35
|
+
'google-play-policy-update',
|
|
36
|
+
'stack-overflow-tips',
|
|
37
|
+
],
|
|
38
|
+
subDirectories: [],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
absolutePath: `${emailsDirectoryPath}/notifications`,
|
|
42
|
+
directoryName: 'notifications',
|
|
43
|
+
relativePath: 'notifications',
|
|
44
|
+
emailFilenames: [
|
|
45
|
+
'github-access-token',
|
|
46
|
+
'papermark-year-in-review',
|
|
47
|
+
'vercel-invite-user',
|
|
48
|
+
'yelp-recent-login',
|
|
49
|
+
],
|
|
50
|
+
subDirectories: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
absolutePath: `${emailsDirectoryPath}/receipts`,
|
|
54
|
+
directoryName: 'receipts',
|
|
55
|
+
relativePath: 'receipts',
|
|
56
|
+
emailFilenames: ['apple-receipt', 'nike-receipt'],
|
|
57
|
+
subDirectories: [],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
absolutePath: `${emailsDirectoryPath}/reset-password`,
|
|
61
|
+
directoryName: 'reset-password',
|
|
62
|
+
relativePath: 'reset-password',
|
|
63
|
+
emailFilenames: ['dropbox-reset-password', 'twitch-reset-password'],
|
|
64
|
+
subDirectories: [],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
absolutePath: `${emailsDirectoryPath}/reviews`,
|
|
68
|
+
directoryName: 'reviews',
|
|
69
|
+
relativePath: 'reviews',
|
|
70
|
+
emailFilenames: ['airbnb-review', 'amazon-review'],
|
|
71
|
+
subDirectories: [],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
absolutePath: `${emailsDirectoryPath}/welcome`,
|
|
75
|
+
directoryName: 'welcome',
|
|
76
|
+
relativePath: 'welcome',
|
|
77
|
+
emailFilenames: ['koala-welcome', 'netlify-welcome', 'stripe-welcome'],
|
|
78
|
+
subDirectories: [],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
expect(containsEmailTemplate('welcome/koala-welcome', directory)).toBe(true);
|
|
83
|
+
expect(containsEmailTemplate('welcome/missing-template', directory)).toBe(
|
|
84
|
+
false,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmailsDirectory } from './get-emails-directory-metadata';
|
|
2
|
+
|
|
3
|
+
export const containsEmailTemplate = (
|
|
4
|
+
relativeEmailPath: string,
|
|
5
|
+
directory: EmailsDirectory,
|
|
6
|
+
) => {
|
|
7
|
+
const remainingSegments = relativeEmailPath
|
|
8
|
+
.replace(directory.relativePath, '')
|
|
9
|
+
.split('/')
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
if (remainingSegments.length === 1) {
|
|
12
|
+
const emailFilename = remainingSegments[0]!;
|
|
13
|
+
return directory.emailFilenames.includes(emailFilename);
|
|
14
|
+
}
|
|
15
|
+
const subDirectory = directory.subDirectories.find(
|
|
16
|
+
(sub) => sub.relativePath === remainingSegments[0],
|
|
17
|
+
);
|
|
18
|
+
if (subDirectory === undefined) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return containsEmailTemplate(relativeEmailPath, subDirectory);
|
|
23
|
+
};
|
|
@@ -28,7 +28,7 @@ export const renderingUtilitiesExporter = (emailTemplates: string[]) => ({
|
|
|
28
28
|
async ({ path: pathToFile }) => {
|
|
29
29
|
return {
|
|
30
30
|
contents: `${await fs.readFile(pathToFile, 'utf8')};
|
|
31
|
-
export { render } from 'react-email-module-that-will-export-render'
|
|
31
|
+
export { render, pretty } from 'react-email-module-that-will-export-render'
|
|
32
32
|
export { createElement as reactEmailCreateReactElement } from 'react';
|
|
33
33
|
`,
|
|
34
34
|
loader: path.extname(pathToFile).slice(1) as Loader,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import type { render } from '@react-email/components';
|
|
2
|
+
import type { pretty, render } from '@react-email/components';
|
|
3
3
|
import { type BuildFailure, type OutputFile, build } from 'esbuild';
|
|
4
4
|
import type React from 'react';
|
|
5
5
|
import type { RawSourceMap } from 'source-map-js';
|
|
@@ -14,6 +14,7 @@ import type { ErrorObject } from './types/error-object';
|
|
|
14
14
|
const EmailComponentModule = z.object({
|
|
15
15
|
default: z.any(),
|
|
16
16
|
render: z.function(),
|
|
17
|
+
pretty: z.function(),
|
|
17
18
|
reactEmailCreateReactElement: z.function(),
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -27,6 +28,8 @@ export const getEmailComponent = async (
|
|
|
27
28
|
|
|
28
29
|
render: typeof render;
|
|
29
30
|
|
|
31
|
+
pretty: typeof pretty;
|
|
32
|
+
|
|
30
33
|
sourceMapToOriginalFile: RawSourceMap;
|
|
31
34
|
}
|
|
32
35
|
| { error: ErrorObject }
|
|
@@ -106,11 +109,27 @@ export const getEmailComponent = async (
|
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
if (typeof parseResult.data.default !== 'function') {
|
|
113
|
+
return {
|
|
114
|
+
error: improveErrorWithSourceMap(
|
|
115
|
+
new Error(
|
|
116
|
+
`The email component at ${emailPath} does not contain a default exported function`,
|
|
117
|
+
{
|
|
118
|
+
cause: parseResult.error,
|
|
119
|
+
},
|
|
120
|
+
),
|
|
121
|
+
emailPath,
|
|
122
|
+
sourceMapToEmail,
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
const { data: componentModule } = parseResult;
|
|
110
128
|
|
|
111
129
|
return {
|
|
112
130
|
emailComponent: componentModule.default as EmailComponent,
|
|
113
131
|
render: componentModule.render as typeof render,
|
|
132
|
+
pretty: componentModule.pretty as typeof pretty,
|
|
114
133
|
createElement:
|
|
115
134
|
componentModule.reactEmailCreateReactElement as typeof React.createElement,
|
|
116
135
|
|
|
@@ -12,7 +12,7 @@ const isFileAnEmail = (fullPath: string): boolean => {
|
|
|
12
12
|
if (!['.js', '.tsx', '.jsx'].includes(ext)) return false;
|
|
13
13
|
|
|
14
14
|
// This is to avoid a possible race condition where the file doesn't exist anymore
|
|
15
|
-
// once we are checking if it is an actual email, this
|
|
15
|
+
// once we are checking if it is an actual email, this could cause issues that
|
|
16
16
|
// would be very hard to debug and find out the why of it happening.
|
|
17
17
|
if (!fs.existsSync(fullPath)) {
|
|
18
18
|
return false;
|