@react-email/preview-server 1.0.0-canary.0
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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +44 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +57 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +41 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +311 -0
- package/.next/routes-manifest.json +64 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/page.js +1 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/preview/[...slug]/page.js +321 -0
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -0
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/134.js +6 -0
- package/.next/server/chunks/235.js +15 -0
- package/.next/server/chunks/278.js +1 -0
- package/.next/server/chunks/343.js +20 -0
- package/.next/server/chunks/428.js +14 -0
- package/.next/server/chunks/963.js +1 -0
- package/.next/server/chunks/999.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +5 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_buildManifest.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_ssgManifest.js +1 -0
- package/.next/static/chunks/107-3043079e7cb8bcae.js +1 -0
- package/.next/static/chunks/293-67391ef0e44ffa4f.js +1 -0
- package/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +1 -0
- package/.next/static/chunks/45-1021fac82f766268.js +1 -0
- package/.next/static/chunks/484-1969fe871b27074e.js +1 -0
- package/.next/static/chunks/484-e02de792ff5f9ea5.js +1 -0
- package/.next/static/chunks/589-817d8691661d370e.js +1 -0
- package/.next/static/chunks/902-c34acb56733e0ce1.js +1 -0
- package/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +1 -0
- package/.next/static/chunks/app/layout-74628781c0b7e7bf.js +1 -0
- package/.next/static/chunks/app/layout-daeba68330ab58bb.js +1 -0
- package/.next/static/chunks/app/page-55cf199b7ca71958.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-07dd9a701d0b3e56.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-61b0ea70a8d72916.js +1 -0
- package/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +6 -0
- package/.next/static/chunks/framework-b887e9fc751a9906.js +1 -0
- package/.next/static/chunks/main-9a03e7ba8acb1900.js +1 -0
- package/.next/static/chunks/main-app-5bc2d814f500db60.js +1 -0
- package/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +1 -0
- package/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-31c45daa2bd82a7b.js +1 -0
- package/.next/static/css/35e8811589f0962b.css +3 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_buildManifest.js +1 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_ssgManifest.js +1 -0
- package/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
- package/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
- package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
- package/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
- package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
- package/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
- package/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/.next/static/media/logo.2ce2a759.png +0 -0
- package/.next/trace +27 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/app/preview/[...slug]/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/_index.js +4 -0
- package/license.md +7 -0
- package/module-punycode.d.ts +3 -0
- package/next-env.d.ts +5 -0
- package/next.config.js +22 -0
- package/package.json +82 -0
- package/postcss.config.js +8 -0
- package/scripts/build-preview-server.mjs +29 -0
- package/scripts/fill-caniemail-data.mjs +36 -0
- package/src/actions/email-validation/caniemail-data.ts +85993 -0
- package/src/actions/email-validation/check-compatibility.ts +333 -0
- package/src/actions/email-validation/check-images.spec.tsx +100 -0
- package/src/actions/email-validation/check-images.ts +160 -0
- package/src/actions/email-validation/check-links.spec.tsx +113 -0
- package/src/actions/email-validation/check-links.ts +113 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/email-validation/quick-fetch.ts +14 -0
- package/src/actions/get-email-path-from-slug.ts +32 -0
- package/src/actions/get-emails-directory-metadata-action.ts +19 -0
- package/src/actions/render-email-by-path.tsx +121 -0
- package/src/animated-icons-data/help.json +1082 -0
- package/src/animated-icons-data/link.json +1309 -0
- package/src/animated-icons-data/load.json +443 -0
- package/src/animated-icons-data/mail.json +1320 -0
- package/src/app/env.ts +15 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
- package/src/app/fonts.ts +39 -0
- package/src/app/globals.css +15 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/logo.png +0 -0
- package/src/app/page.tsx +46 -0
- package/src/app/preview/[...slug]/page.tsx +157 -0
- package/src/app/preview/[...slug]/preview.tsx +216 -0
- package/src/app/preview/[...slug]/rendering-error.tsx +40 -0
- package/src/components/button.tsx +101 -0
- package/src/components/code-container.tsx +164 -0
- package/src/components/code-snippet.tsx +9 -0
- package/src/components/code.tsx +184 -0
- package/src/components/heading.tsx +113 -0
- package/src/components/icons/icon-arrow-down.tsx +16 -0
- package/src/components/icons/icon-base.tsx +26 -0
- package/src/components/icons/icon-bug.tsx +19 -0
- package/src/components/icons/icon-button.tsx +23 -0
- package/src/components/icons/icon-check.tsx +19 -0
- package/src/components/icons/icon-clipboard.tsx +40 -0
- package/src/components/icons/icon-download.tsx +19 -0
- package/src/components/icons/icon-email.tsx +18 -0
- package/src/components/icons/icon-file.tsx +19 -0
- package/src/components/icons/icon-folder-open.tsx +19 -0
- package/src/components/icons/icon-folder.tsx +18 -0
- package/src/components/icons/icon-hide-sidebar.tsx +23 -0
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-link.tsx +14 -0
- package/src/components/icons/icon-monitor.tsx +19 -0
- package/src/components/icons/icon-phone.tsx +26 -0
- package/src/components/icons/icon-reload.tsx +18 -0
- package/src/components/icons/icon-source.tsx +19 -0
- package/src/components/icons/icon-stamp.tsx +14 -0
- package/src/components/icons/icon-warning.tsx +31 -0
- package/src/components/index.ts +7 -0
- package/src/components/logo.tsx +63 -0
- package/src/components/resizable-wrapper.tsx +173 -0
- package/src/components/send.tsx +134 -0
- package/src/components/shell.tsx +92 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +139 -0
- package/src/components/sidebar/file-tree-directory.tsx +92 -0
- package/src/components/sidebar/file-tree.tsx +31 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar.tsx +43 -0
- package/src/components/text.tsx +99 -0
- package/src/components/toolbar/checking-results.tsx +150 -0
- package/src/components/toolbar/code-preview-line-link.tsx +40 -0
- package/src/components/toolbar/compatibility.tsx +113 -0
- package/src/components/toolbar/linter.tsx +278 -0
- package/src/components/toolbar/results-table.tsx +0 -0
- package/src/components/toolbar/results.tsx +51 -0
- package/src/components/toolbar/spam-assassin.tsx +155 -0
- package/src/components/toolbar/toolbar-button.tsx +52 -0
- package/src/components/toolbar/use-cached-state.ts +33 -0
- package/src/components/toolbar.tsx +349 -0
- package/src/components/tooltip-content.tsx +31 -0
- package/src/components/tooltip.tsx +19 -0
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +59 -0
- package/src/contexts/emails.tsx +59 -0
- package/src/contexts/fragment-identifier.tsx +48 -0
- package/src/contexts/preview.tsx +79 -0
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-email-rendering-result.ts +58 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/hooks/use-hot-reload.ts +31 -0
- package/src/hooks/use-icon-animation.ts +41 -0
- package/src/hooks/use-rendering-metadata.ts +36 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
- package/src/utils/caniemail/all-css-properties.ts +358 -0
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
- package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
- package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
- package/src/utils/caniemail/get-css-functions.ts +25 -0
- package/src/utils/caniemail/get-css-property-names.ts +32 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
- package/src/utils/caniemail/get-css-unit.ts +3 -0
- package/src/utils/caniemail/get-element-attributes.ts +7 -0
- package/src/utils/caniemail/get-element-names.ts +20 -0
- package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +187 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
- package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/constants.ts +6 -0
- package/src/utils/contains-email-template.spec.ts +124 -0
- package/src/utils/contains-email-template.ts +33 -0
- package/src/utils/copy-text-to-clipboard.ts +7 -0
- package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
- package/src/utils/get-email-component.spec.ts +41 -0
- package/src/utils/get-email-component.ts +134 -0
- package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
- package/src/utils/get-emails-directory-metadata.ts +141 -0
- package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
- package/src/utils/get-line-and-column-from-offset.ts +11 -0
- package/src/utils/improve-error-with-sourcemap.ts +85 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/js-email-detection.spec.ts +24 -0
- package/src/utils/language-map.ts +7 -0
- package/src/utils/linting.ts +60 -0
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/register-spinner-autostopping.ts +28 -0
- package/src/utils/result.ts +49 -0
- package/src/utils/run-bundled-code.ts +64 -0
- package/src/utils/sanitize.ts +6 -0
- package/src/utils/static-node-modules-for-vm.ts +93 -0
- package/src/utils/testing/js-email-export-default.js +17 -0
- package/src/utils/testing/js-email-test.js +18 -0
- package/src/utils/testing/mdx-email-test.js +128 -0
- package/src/utils/testing/request-response-email.tsx +9 -0
- package/src/utils/types/as.ts +26 -0
- package/src/utils/types/email-template.ts +8 -0
- package/src/utils/types/error-object.ts +11 -0
- package/src/utils/types/hot-reload-change.ts +13 -0
- package/src/utils/unreachable.ts +8 -0
- package/tailwind-internals.d.ts +133 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +45 -0
- package/vitest.config.ts +15 -0
package/src/app/env.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
/** ONLY ACCESSIBLE ON THE SERVER */
|
|
3
|
+
export const emailsDirRelativePath = process.env.EMAILS_DIR_RELATIVE_PATH!;
|
|
4
|
+
|
|
5
|
+
/** ONLY ACCESSIBLE ON THE SERVER */
|
|
6
|
+
export const userProjectLocation = process.env.USER_PROJECT_LOCATION!;
|
|
7
|
+
|
|
8
|
+
/** ONLY ACCESSIBLE ON THE SERVER */
|
|
9
|
+
export const emailsDirectoryAbsolutePath =
|
|
10
|
+
process.env.EMAILS_DIR_ABSOLUTE_PATH!;
|
|
11
|
+
|
|
12
|
+
export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
|
|
13
|
+
|
|
14
|
+
export const isPreviewDevelopment =
|
|
15
|
+
process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true';
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/app/fonts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Inter } from 'next/font/google';
|
|
2
|
+
import Local from 'next/font/local';
|
|
3
|
+
|
|
4
|
+
export const inter = Inter({
|
|
5
|
+
subsets: ['latin'],
|
|
6
|
+
variable: '--font-inter',
|
|
7
|
+
display: 'swap',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const sfMono = Local({
|
|
11
|
+
src: [
|
|
12
|
+
{
|
|
13
|
+
path: './fonts/SFMono/SFMonoLight.otf',
|
|
14
|
+
weight: '300',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
path: './fonts/SFMono/SFMonoRegular.otf',
|
|
18
|
+
weight: '400',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: './fonts/SFMono/SFMonoMedium.otf',
|
|
22
|
+
weight: '500',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: './fonts/SFMono/SFMonoSemibold.otf',
|
|
26
|
+
weight: '600',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: './fonts/SFMono/SFMonoBold.otf',
|
|
30
|
+
weight: '700',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: './fonts/SFMono/SFMonoHeavy.otf',
|
|
34
|
+
weight: '800',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
variable: '--font-sf-mono',
|
|
38
|
+
display: 'swap',
|
|
39
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
import { EmailsProvider } from '../contexts/emails';
|
|
4
|
+
import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata';
|
|
5
|
+
import { emailsDirectoryAbsolutePath } from './env';
|
|
6
|
+
import { inter, sfMono } from './fonts';
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: 'React Email',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
|
15
|
+
const emailsDirectoryMetadata = await getEmailsDirectoryMetadata(
|
|
16
|
+
emailsDirectoryAbsolutePath,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (typeof emailsDirectoryMetadata === 'undefined') {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Could not find the emails directory specified under ${emailsDirectoryAbsolutePath}!`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<html
|
|
27
|
+
className={`${inter.variable} ${sfMono.variable} font-sans`}
|
|
28
|
+
lang="en"
|
|
29
|
+
>
|
|
30
|
+
<body className="relative flex h-screen flex-col bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
|
|
31
|
+
<div className="bg-gradient-to-t from-slate-3 flex">
|
|
32
|
+
<EmailsProvider
|
|
33
|
+
initialEmailsDirectoryMetadata={emailsDirectoryMetadata}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</EmailsProvider>
|
|
37
|
+
</div>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default RootLayout;
|
package/src/app/logo.png
ADDED
|
Binary file
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { Button, Heading, Text } from '../components';
|
|
5
|
+
import CodeSnippet from '../components/code-snippet';
|
|
6
|
+
import { Shell } from '../components/shell';
|
|
7
|
+
import { emailsDirectoryAbsolutePath } from './env';
|
|
8
|
+
import logo from './logo.png';
|
|
9
|
+
|
|
10
|
+
const Home = () => {
|
|
11
|
+
const baseEmailsDirectoryName = path.basename(emailsDirectoryAbsolutePath);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Shell>
|
|
15
|
+
<div className="w-full h-full flex items-center justify-center p-8">
|
|
16
|
+
<div className="-mt-10 relative max-w-lg flex flex-col items-center gap-3 text-center">
|
|
17
|
+
<Image
|
|
18
|
+
alt="React Email Icon"
|
|
19
|
+
className="mb-8"
|
|
20
|
+
height={144}
|
|
21
|
+
src={logo}
|
|
22
|
+
style={{
|
|
23
|
+
borderRadius: 34,
|
|
24
|
+
boxShadow: '0 .625rem 12.5rem 1.25rem #2B7CA080',
|
|
25
|
+
}}
|
|
26
|
+
width={141}
|
|
27
|
+
/>
|
|
28
|
+
<Heading as="h2" size="6" weight="medium">
|
|
29
|
+
Welcome to React Email
|
|
30
|
+
</Heading>
|
|
31
|
+
<Text as="p">
|
|
32
|
+
To start developing your emails, you can create a<br />
|
|
33
|
+
<CodeSnippet>.jsx</CodeSnippet> or <CodeSnippet>.tsx</CodeSnippet>{' '}
|
|
34
|
+
file under your <CodeSnippet>{baseEmailsDirectoryName}</CodeSnippet>{' '}
|
|
35
|
+
folder.
|
|
36
|
+
</Text>
|
|
37
|
+
<Button asChild className="mt-3" size="3">
|
|
38
|
+
<Link href="https://react.email/docs">Check the docs</Link>
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</Shell>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Home;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
type CompatibilityCheckingResult,
|
|
6
|
+
checkCompatibility,
|
|
7
|
+
} from '../../../actions/email-validation/check-compatibility';
|
|
8
|
+
import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
|
|
9
|
+
import { renderEmailByPath } from '../../../actions/render-email-by-path';
|
|
10
|
+
import { Shell } from '../../../components/shell';
|
|
11
|
+
import { Toolbar } from '../../../components/toolbar';
|
|
12
|
+
import type { LintingRow } from '../../../components/toolbar/linter';
|
|
13
|
+
import type { SpamCheckingResult } from '../../../components/toolbar/spam-assassin';
|
|
14
|
+
import { PreviewProvider } from '../../../contexts/preview';
|
|
15
|
+
import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
|
|
16
|
+
import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
|
|
17
|
+
import { loadStream } from '../../../utils/load-stream';
|
|
18
|
+
import { emailsDirectoryAbsolutePath, isBuilding } from '../../env';
|
|
19
|
+
import Preview from './preview';
|
|
20
|
+
|
|
21
|
+
export const dynamicParams = true;
|
|
22
|
+
|
|
23
|
+
export const dynamic = 'force-dynamic';
|
|
24
|
+
|
|
25
|
+
export interface PreviewParams {
|
|
26
|
+
slug: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const Page = async ({
|
|
30
|
+
params: paramsPromise,
|
|
31
|
+
}: {
|
|
32
|
+
params: Promise<PreviewParams>;
|
|
33
|
+
}) => {
|
|
34
|
+
const params = await paramsPromise;
|
|
35
|
+
// will come in here as segments of a relative path to the email
|
|
36
|
+
// ex: ['authentication', 'verify-password.tsx']
|
|
37
|
+
const slug = decodeURIComponent(params.slug.join('/'));
|
|
38
|
+
const emailsDirMetadata = await getEmailsDirectoryMetadata(
|
|
39
|
+
emailsDirectoryAbsolutePath,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (typeof emailsDirMetadata === 'undefined') {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Could not find the emails directory specified under ${emailsDirectoryAbsolutePath}!
|
|
45
|
+
|
|
46
|
+
This is most likely not an issue with the preview server. Maybe there was a typo on the "--dir" flag?`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let emailPath: string;
|
|
51
|
+
try {
|
|
52
|
+
emailPath = await getEmailPathFromSlug(slug);
|
|
53
|
+
} catch (exception) {
|
|
54
|
+
if (exception instanceof Error) {
|
|
55
|
+
console.warn(exception.message);
|
|
56
|
+
redirect('/');
|
|
57
|
+
}
|
|
58
|
+
throw exception;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const serverEmailRenderingResult = await renderEmailByPath(emailPath);
|
|
62
|
+
|
|
63
|
+
let spamCheckingResult: SpamCheckingResult | undefined = undefined;
|
|
64
|
+
let lintingRows: LintingRow[] | undefined = undefined;
|
|
65
|
+
let compatibilityCheckingResults: CompatibilityCheckingResult[] | undefined =
|
|
66
|
+
undefined;
|
|
67
|
+
|
|
68
|
+
if (isBuilding) {
|
|
69
|
+
if ('error' in serverEmailRenderingResult) {
|
|
70
|
+
throw new Error(serverEmailRenderingResult.error.message, {
|
|
71
|
+
cause: serverEmailRenderingResult.error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const lintingSources = getLintingSources(
|
|
75
|
+
serverEmailRenderingResult.markup,
|
|
76
|
+
'',
|
|
77
|
+
);
|
|
78
|
+
lintingRows = [];
|
|
79
|
+
for await (const row of loadLintingRowsFrom(lintingSources)) {
|
|
80
|
+
lintingRows.push(row);
|
|
81
|
+
}
|
|
82
|
+
lintingRows.sort((a, b) => {
|
|
83
|
+
if (a.result.status === 'error' && b.result.status === 'warning') {
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (a.result.status === 'warning' && b.result.status === 'error') {
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return 0;
|
|
92
|
+
});
|
|
93
|
+
compatibilityCheckingResults = [];
|
|
94
|
+
for await (const result of loadStream(
|
|
95
|
+
await checkCompatibility(
|
|
96
|
+
serverEmailRenderingResult.reactMarkup,
|
|
97
|
+
emailPath,
|
|
98
|
+
),
|
|
99
|
+
)) {
|
|
100
|
+
compatibilityCheckingResults.push(result);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const response = await fetch('https://react.email/api/check-spam', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
html: serverEmailRenderingResult.markup,
|
|
108
|
+
plainText: serverEmailRenderingResult.plainText,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const responseBody = (await response.json()) as
|
|
112
|
+
| { error: string }
|
|
113
|
+
| SpamCheckingResult;
|
|
114
|
+
if ('error' in responseBody) {
|
|
115
|
+
throw new Error(`Failed doing Spam Check. ${responseBody.error}`, {
|
|
116
|
+
cause: responseBody,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
spamCheckingResult = responseBody;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<PreviewProvider
|
|
125
|
+
emailSlug={slug}
|
|
126
|
+
emailPath={emailPath}
|
|
127
|
+
serverRenderingResult={serverEmailRenderingResult}
|
|
128
|
+
>
|
|
129
|
+
<Shell currentEmailOpenSlug={slug}>
|
|
130
|
+
{/* This suspense is so that this page doesn't throw warnings */}
|
|
131
|
+
{/* on the build of the preview server de-opting into */}
|
|
132
|
+
{/* client-side rendering on build */}
|
|
133
|
+
<Suspense>
|
|
134
|
+
<Preview emailTitle={path.basename(emailPath)} />
|
|
135
|
+
|
|
136
|
+
<Toolbar
|
|
137
|
+
serverLintingRows={lintingRows}
|
|
138
|
+
serverSpamCheckingResult={spamCheckingResult}
|
|
139
|
+
serverCompatibilityResults={compatibilityCheckingResults}
|
|
140
|
+
/>
|
|
141
|
+
</Suspense>
|
|
142
|
+
</Shell>
|
|
143
|
+
</PreviewProvider>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export async function generateMetadata({
|
|
148
|
+
params,
|
|
149
|
+
}: {
|
|
150
|
+
params: Promise<PreviewParams>;
|
|
151
|
+
}) {
|
|
152
|
+
const { slug } = await params;
|
|
153
|
+
|
|
154
|
+
return { title: `${path.basename(slug.join('/'))} — React Email` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default Page;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
+
import { use, useState } from 'react';
|
|
5
|
+
import { flushSync } from 'react-dom';
|
|
6
|
+
import { Toaster } from 'sonner';
|
|
7
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
8
|
+
import { Topbar } from '../../../components';
|
|
9
|
+
import { CodeContainer } from '../../../components/code-container';
|
|
10
|
+
import {
|
|
11
|
+
makeIframeDocumentBubbleEvents,
|
|
12
|
+
ResizableWrapper,
|
|
13
|
+
} from '../../../components/resizable-wrapper';
|
|
14
|
+
import { Send } from '../../../components/send';
|
|
15
|
+
import { useToolbarState } from '../../../components/toolbar';
|
|
16
|
+
import { Tooltip } from '../../../components/tooltip';
|
|
17
|
+
import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
|
|
18
|
+
import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
|
|
19
|
+
import { PreviewContext } from '../../../contexts/preview';
|
|
20
|
+
import { useClampedState } from '../../../hooks/use-clamped-state';
|
|
21
|
+
import { cn } from '../../../utils';
|
|
22
|
+
import { RenderingError } from './rendering-error';
|
|
23
|
+
|
|
24
|
+
interface PreviewProps extends React.ComponentProps<'div'> {
|
|
25
|
+
emailTitle: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
29
|
+
const { renderingResult, renderedEmailMetadata } = use(PreviewContext)!;
|
|
30
|
+
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const pathname = usePathname();
|
|
33
|
+
const searchParams = useSearchParams();
|
|
34
|
+
|
|
35
|
+
const activeView = searchParams.get('view') ?? 'preview';
|
|
36
|
+
const activeLang = searchParams.get('lang') ?? 'jsx';
|
|
37
|
+
|
|
38
|
+
const handleViewChange = (view: string) => {
|
|
39
|
+
const params = new URLSearchParams(searchParams);
|
|
40
|
+
params.set('view', view);
|
|
41
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleLangChange = (lang: string) => {
|
|
45
|
+
const params = new URLSearchParams(searchParams);
|
|
46
|
+
params.set('view', 'source');
|
|
47
|
+
params.set('lang', lang);
|
|
48
|
+
const isSameLang = searchParams.get('lang') === lang;
|
|
49
|
+
router.push(
|
|
50
|
+
`${pathname}?${params.toString()}${isSameLang ? location.hash : ''}`,
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
|
|
55
|
+
const hasErrors = 'error' in renderingResult;
|
|
56
|
+
|
|
57
|
+
const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
|
|
58
|
+
const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
|
|
59
|
+
const minWidth = 100;
|
|
60
|
+
const minHeight = 100;
|
|
61
|
+
const storedWidth = searchParams.get('width');
|
|
62
|
+
const storedHeight = searchParams.get('height');
|
|
63
|
+
const [width, setWidth] = useClampedState(
|
|
64
|
+
storedWidth ? Number.parseInt(storedWidth) : 600,
|
|
65
|
+
minWidth,
|
|
66
|
+
maxWidth,
|
|
67
|
+
);
|
|
68
|
+
const [height, setHeight] = useClampedState(
|
|
69
|
+
storedHeight ? Number.parseInt(storedHeight) : 1024,
|
|
70
|
+
minHeight,
|
|
71
|
+
maxHeight,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const handleSaveViewSize = useDebouncedCallback(() => {
|
|
75
|
+
const params = new URLSearchParams(searchParams);
|
|
76
|
+
params.set('width', width.toString());
|
|
77
|
+
params.set('height', height.toString());
|
|
78
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
79
|
+
}, 300);
|
|
80
|
+
|
|
81
|
+
const { toggled: toolbarToggled } = useToolbarState();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<Topbar emailTitle={emailTitle}>
|
|
86
|
+
<ViewSizeControls
|
|
87
|
+
setViewHeight={(height) => {
|
|
88
|
+
setHeight(height);
|
|
89
|
+
flushSync(() => {
|
|
90
|
+
handleSaveViewSize();
|
|
91
|
+
});
|
|
92
|
+
}}
|
|
93
|
+
setViewWidth={(width) => {
|
|
94
|
+
setWidth(width);
|
|
95
|
+
flushSync(() => {
|
|
96
|
+
handleSaveViewSize();
|
|
97
|
+
});
|
|
98
|
+
}}
|
|
99
|
+
viewHeight={height}
|
|
100
|
+
viewWidth={width}
|
|
101
|
+
/>
|
|
102
|
+
<ActiveViewToggleGroup
|
|
103
|
+
activeView={activeView}
|
|
104
|
+
setActiveView={handleViewChange}
|
|
105
|
+
/>
|
|
106
|
+
{hasRenderingMetadata ? (
|
|
107
|
+
<div className="flex justify-end">
|
|
108
|
+
<Send markup={renderedEmailMetadata.markup} />
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</Topbar>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
{...props}
|
|
115
|
+
className={cn(
|
|
116
|
+
'h-[calc(100%-3.5rem-2.375rem)] will-change-[height] flex p-4 transition-[height] duration-300',
|
|
117
|
+
activeView === 'preview' && 'bg-gray-200',
|
|
118
|
+
toolbarToggled && 'h-[calc(100%-3.5rem-13rem)]',
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
ref={(element) => {
|
|
122
|
+
const observer = new ResizeObserver((entry) => {
|
|
123
|
+
const [elementEntry] = entry;
|
|
124
|
+
if (elementEntry) {
|
|
125
|
+
setMaxWidth(elementEntry.contentRect.width);
|
|
126
|
+
setMaxHeight(elementEntry.contentRect.height);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (element) {
|
|
131
|
+
observer.observe(element);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
observer.disconnect();
|
|
136
|
+
};
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{hasErrors ? <RenderingError error={renderingResult.error} /> : null}
|
|
140
|
+
|
|
141
|
+
{hasRenderingMetadata ? (
|
|
142
|
+
<>
|
|
143
|
+
{activeView === 'preview' && (
|
|
144
|
+
<ResizableWrapper
|
|
145
|
+
minHeight={minHeight}
|
|
146
|
+
minWidth={minWidth}
|
|
147
|
+
maxHeight={maxHeight}
|
|
148
|
+
maxWidth={maxWidth}
|
|
149
|
+
height={height}
|
|
150
|
+
onResizeEnd={() => {
|
|
151
|
+
handleSaveViewSize();
|
|
152
|
+
}}
|
|
153
|
+
onResize={(value, direction) => {
|
|
154
|
+
const isHorizontal =
|
|
155
|
+
direction === 'east' || direction === 'west';
|
|
156
|
+
if (isHorizontal) {
|
|
157
|
+
setWidth(Math.round(value));
|
|
158
|
+
} else {
|
|
159
|
+
setHeight(Math.round(value));
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
162
|
+
width={width}
|
|
163
|
+
>
|
|
164
|
+
<iframe
|
|
165
|
+
className="solid max-h-full rounded-lg bg-white"
|
|
166
|
+
ref={(iframe) => {
|
|
167
|
+
if (iframe) {
|
|
168
|
+
return makeIframeDocumentBubbleEvents(iframe);
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
srcDoc={renderedEmailMetadata.markup}
|
|
172
|
+
style={{
|
|
173
|
+
width: `${width}px`,
|
|
174
|
+
height: `${height}px`,
|
|
175
|
+
}}
|
|
176
|
+
title={emailTitle}
|
|
177
|
+
/>
|
|
178
|
+
</ResizableWrapper>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{activeView === 'source' && (
|
|
182
|
+
<div className="h-full w-full">
|
|
183
|
+
<div className="m-auto h-full flex max-w-3xl p-6">
|
|
184
|
+
<Tooltip.Provider>
|
|
185
|
+
<CodeContainer
|
|
186
|
+
activeLang={activeLang}
|
|
187
|
+
markups={[
|
|
188
|
+
{
|
|
189
|
+
language: 'jsx',
|
|
190
|
+
content: renderedEmailMetadata.reactMarkup,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
language: 'markup',
|
|
194
|
+
content: renderedEmailMetadata.markup,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
language: 'markdown',
|
|
198
|
+
content: renderedEmailMetadata.plainText,
|
|
199
|
+
},
|
|
200
|
+
]}
|
|
201
|
+
setActiveLang={handleLangChange}
|
|
202
|
+
/>
|
|
203
|
+
</Tooltip.Provider>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</>
|
|
208
|
+
) : null}
|
|
209
|
+
|
|
210
|
+
<Toaster />
|
|
211
|
+
</div>
|
|
212
|
+
</>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default Preview;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ErrorObject } from '../../../utils/types/error-object';
|
|
3
|
+
|
|
4
|
+
export const RenderingError = (props: { error: ErrorObject }) => {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<div className="absolute inset-0 z-50 bg-black/80" />
|
|
8
|
+
<div className="absolute left-[50%] top-[50%] z-50 grid min-h-[50vh] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-t-sm border border-t-4 bg-white p-6 text-black shadow-lg duration-200 sm:rounded-lg md:max-w-[568px] lg:max-w-[968px]">
|
|
9
|
+
<div className="flex min-w-0 max-w-full flex-col space-y-1.5">
|
|
10
|
+
<h2 className="flex flex-shrink items-center gap-4 pb-2 text-lg font-semibold leading-none tracking-tight">
|
|
11
|
+
<svg
|
|
12
|
+
className="h-6 w-6 font-extrabold text-red-600"
|
|
13
|
+
fill="none"
|
|
14
|
+
height="24"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
width="24"
|
|
21
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
+
>
|
|
23
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
24
|
+
<path d="M12 9v4" />
|
|
25
|
+
<path d="M12 17h.01" />
|
|
26
|
+
</svg>
|
|
27
|
+
{props.error.name}: {props.error.message}
|
|
28
|
+
</h2>
|
|
29
|
+
{props.error.stack ? (
|
|
30
|
+
<div className="flex-grow scroll-px-4 overflow-x-auto rounded-lg bg-red-500 p-2 text-sm text-gray-100">
|
|
31
|
+
<pre className="w-full min-w-0 font-mono leading-7">
|
|
32
|
+
{props.error.stack}
|
|
33
|
+
</pre>
|
|
34
|
+
</div>
|
|
35
|
+
) : undefined}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
};
|