@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.
Files changed (265) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +44 -0
  3. package/.next/app-path-routes-manifest.json +6 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/diagnostics/build-diagnostics.json +6 -0
  6. package/.next/diagnostics/framework.json +1 -0
  7. package/.next/export-marker.json +6 -0
  8. package/.next/images-manifest.json +57 -0
  9. package/.next/next-minimal-server.js.nft.json +1 -0
  10. package/.next/next-server.js.nft.json +1 -0
  11. package/.next/package.json +1 -0
  12. package/.next/prerender-manifest.json +41 -0
  13. package/.next/react-loadable-manifest.json +1 -0
  14. package/.next/required-server-files.json +311 -0
  15. package/.next/routes-manifest.json +64 -0
  16. package/.next/server/app/_not-found/page.js +1 -0
  17. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  18. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  19. package/.next/server/app/favicon.ico/route.js +1 -0
  20. package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  21. package/.next/server/app/favicon.ico.body +0 -0
  22. package/.next/server/app/favicon.ico.meta +1 -0
  23. package/.next/server/app/page.js +1 -0
  24. package/.next/server/app/page.js.nft.json +1 -0
  25. package/.next/server/app/page_client-reference-manifest.js +1 -0
  26. package/.next/server/app/preview/[...slug]/page.js +321 -0
  27. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -0
  28. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -0
  29. package/.next/server/app-paths-manifest.json +6 -0
  30. package/.next/server/chunks/134.js +6 -0
  31. package/.next/server/chunks/235.js +15 -0
  32. package/.next/server/chunks/278.js +1 -0
  33. package/.next/server/chunks/343.js +20 -0
  34. package/.next/server/chunks/428.js +14 -0
  35. package/.next/server/chunks/963.js +1 -0
  36. package/.next/server/chunks/999.js +1 -0
  37. package/.next/server/functions-config-manifest.json +4 -0
  38. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  39. package/.next/server/middleware-build-manifest.js +1 -0
  40. package/.next/server/middleware-manifest.json +6 -0
  41. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  42. package/.next/server/next-font-manifest.js +1 -0
  43. package/.next/server/next-font-manifest.json +1 -0
  44. package/.next/server/pages/500.html +1 -0
  45. package/.next/server/pages/_app.js +1 -0
  46. package/.next/server/pages/_app.js.nft.json +1 -0
  47. package/.next/server/pages/_document.js +1 -0
  48. package/.next/server/pages/_document.js.nft.json +1 -0
  49. package/.next/server/pages/_error.js +1 -0
  50. package/.next/server/pages/_error.js.nft.json +1 -0
  51. package/.next/server/pages-manifest.json +5 -0
  52. package/.next/server/server-reference-manifest.js +1 -0
  53. package/.next/server/server-reference-manifest.json +1 -0
  54. package/.next/server/webpack-runtime.js +1 -0
  55. package/.next/static/VkyJa9K30jCrKBesOgrQT/_buildManifest.js +1 -0
  56. package/.next/static/VkyJa9K30jCrKBesOgrQT/_ssgManifest.js +1 -0
  57. package/.next/static/chunks/107-3043079e7cb8bcae.js +1 -0
  58. package/.next/static/chunks/293-67391ef0e44ffa4f.js +1 -0
  59. package/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +1 -0
  60. package/.next/static/chunks/45-1021fac82f766268.js +1 -0
  61. package/.next/static/chunks/484-1969fe871b27074e.js +1 -0
  62. package/.next/static/chunks/484-e02de792ff5f9ea5.js +1 -0
  63. package/.next/static/chunks/589-817d8691661d370e.js +1 -0
  64. package/.next/static/chunks/902-c34acb56733e0ce1.js +1 -0
  65. package/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +1 -0
  66. package/.next/static/chunks/app/layout-74628781c0b7e7bf.js +1 -0
  67. package/.next/static/chunks/app/layout-daeba68330ab58bb.js +1 -0
  68. package/.next/static/chunks/app/page-55cf199b7ca71958.js +1 -0
  69. package/.next/static/chunks/app/preview/[...slug]/page-07dd9a701d0b3e56.js +1 -0
  70. package/.next/static/chunks/app/preview/[...slug]/page-61b0ea70a8d72916.js +1 -0
  71. package/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +6 -0
  72. package/.next/static/chunks/framework-b887e9fc751a9906.js +1 -0
  73. package/.next/static/chunks/main-9a03e7ba8acb1900.js +1 -0
  74. package/.next/static/chunks/main-app-5bc2d814f500db60.js +1 -0
  75. package/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +1 -0
  76. package/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +1 -0
  77. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  78. package/.next/static/chunks/webpack-31c45daa2bd82a7b.js +1 -0
  79. package/.next/static/css/35e8811589f0962b.css +3 -0
  80. package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_buildManifest.js +1 -0
  81. package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_ssgManifest.js +1 -0
  82. package/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
  83. package/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
  84. package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
  85. package/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
  86. package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
  87. package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
  88. package/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
  89. package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
  90. package/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
  91. package/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
  92. package/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
  93. package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  94. package/.next/static/media/e4051546b3043204-s.p.otf +0 -0
  95. package/.next/static/media/logo.2ce2a759.png +0 -0
  96. package/.next/trace +27 -0
  97. package/.next/types/app/layout.ts +84 -0
  98. package/.next/types/app/page.ts +84 -0
  99. package/.next/types/app/preview/[...slug]/page.ts +84 -0
  100. package/.next/types/cache-life.d.ts +141 -0
  101. package/.next/types/package.json +1 -0
  102. package/_index.js +4 -0
  103. package/license.md +7 -0
  104. package/module-punycode.d.ts +3 -0
  105. package/next-env.d.ts +5 -0
  106. package/next.config.js +22 -0
  107. package/package.json +82 -0
  108. package/postcss.config.js +8 -0
  109. package/scripts/build-preview-server.mjs +29 -0
  110. package/scripts/fill-caniemail-data.mjs +36 -0
  111. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  112. package/src/actions/email-validation/check-compatibility.ts +333 -0
  113. package/src/actions/email-validation/check-images.spec.tsx +100 -0
  114. package/src/actions/email-validation/check-images.ts +160 -0
  115. package/src/actions/email-validation/check-links.spec.tsx +113 -0
  116. package/src/actions/email-validation/check-links.ts +113 -0
  117. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  118. package/src/actions/email-validation/quick-fetch.ts +14 -0
  119. package/src/actions/get-email-path-from-slug.ts +32 -0
  120. package/src/actions/get-emails-directory-metadata-action.ts +19 -0
  121. package/src/actions/render-email-by-path.tsx +121 -0
  122. package/src/animated-icons-data/help.json +1082 -0
  123. package/src/animated-icons-data/link.json +1309 -0
  124. package/src/animated-icons-data/load.json +443 -0
  125. package/src/animated-icons-data/mail.json +1320 -0
  126. package/src/app/env.ts +15 -0
  127. package/src/app/favicon.ico +0 -0
  128. package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
  129. package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
  130. package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
  131. package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
  132. package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
  133. package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
  134. package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
  135. package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
  136. package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
  137. package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
  138. package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
  139. package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
  140. package/src/app/fonts.ts +39 -0
  141. package/src/app/globals.css +15 -0
  142. package/src/app/layout.tsx +43 -0
  143. package/src/app/logo.png +0 -0
  144. package/src/app/page.tsx +46 -0
  145. package/src/app/preview/[...slug]/page.tsx +157 -0
  146. package/src/app/preview/[...slug]/preview.tsx +216 -0
  147. package/src/app/preview/[...slug]/rendering-error.tsx +40 -0
  148. package/src/components/button.tsx +101 -0
  149. package/src/components/code-container.tsx +164 -0
  150. package/src/components/code-snippet.tsx +9 -0
  151. package/src/components/code.tsx +184 -0
  152. package/src/components/heading.tsx +113 -0
  153. package/src/components/icons/icon-arrow-down.tsx +16 -0
  154. package/src/components/icons/icon-base.tsx +26 -0
  155. package/src/components/icons/icon-bug.tsx +19 -0
  156. package/src/components/icons/icon-button.tsx +23 -0
  157. package/src/components/icons/icon-check.tsx +19 -0
  158. package/src/components/icons/icon-clipboard.tsx +40 -0
  159. package/src/components/icons/icon-download.tsx +19 -0
  160. package/src/components/icons/icon-email.tsx +18 -0
  161. package/src/components/icons/icon-file.tsx +19 -0
  162. package/src/components/icons/icon-folder-open.tsx +19 -0
  163. package/src/components/icons/icon-folder.tsx +18 -0
  164. package/src/components/icons/icon-hide-sidebar.tsx +23 -0
  165. package/src/components/icons/icon-image.tsx +19 -0
  166. package/src/components/icons/icon-info.tsx +18 -0
  167. package/src/components/icons/icon-link.tsx +14 -0
  168. package/src/components/icons/icon-monitor.tsx +19 -0
  169. package/src/components/icons/icon-phone.tsx +26 -0
  170. package/src/components/icons/icon-reload.tsx +18 -0
  171. package/src/components/icons/icon-source.tsx +19 -0
  172. package/src/components/icons/icon-stamp.tsx +14 -0
  173. package/src/components/icons/icon-warning.tsx +31 -0
  174. package/src/components/index.ts +7 -0
  175. package/src/components/logo.tsx +63 -0
  176. package/src/components/resizable-wrapper.tsx +173 -0
  177. package/src/components/send.tsx +134 -0
  178. package/src/components/shell.tsx +92 -0
  179. package/src/components/sidebar/file-tree-directory-children.tsx +139 -0
  180. package/src/components/sidebar/file-tree-directory.tsx +92 -0
  181. package/src/components/sidebar/file-tree.tsx +31 -0
  182. package/src/components/sidebar/index.ts +1 -0
  183. package/src/components/sidebar/sidebar.tsx +43 -0
  184. package/src/components/text.tsx +99 -0
  185. package/src/components/toolbar/checking-results.tsx +150 -0
  186. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  187. package/src/components/toolbar/compatibility.tsx +113 -0
  188. package/src/components/toolbar/linter.tsx +278 -0
  189. package/src/components/toolbar/results-table.tsx +0 -0
  190. package/src/components/toolbar/results.tsx +51 -0
  191. package/src/components/toolbar/spam-assassin.tsx +155 -0
  192. package/src/components/toolbar/toolbar-button.tsx +52 -0
  193. package/src/components/toolbar/use-cached-state.ts +33 -0
  194. package/src/components/toolbar.tsx +349 -0
  195. package/src/components/tooltip-content.tsx +31 -0
  196. package/src/components/tooltip.tsx +19 -0
  197. package/src/components/topbar/active-view-toggle-group.tsx +86 -0
  198. package/src/components/topbar/view-size-controls.tsx +247 -0
  199. package/src/components/topbar.tsx +59 -0
  200. package/src/contexts/emails.tsx +59 -0
  201. package/src/contexts/fragment-identifier.tsx +48 -0
  202. package/src/contexts/preview.tsx +79 -0
  203. package/src/hooks/use-clamped-state.ts +24 -0
  204. package/src/hooks/use-email-rendering-result.ts +58 -0
  205. package/src/hooks/use-fragment-identifier.ts +14 -0
  206. package/src/hooks/use-hot-reload.ts +31 -0
  207. package/src/hooks/use-icon-animation.ts +41 -0
  208. package/src/hooks/use-rendering-metadata.ts +36 -0
  209. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
  210. package/src/utils/caniemail/all-css-properties.ts +358 -0
  211. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  212. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  213. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  214. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  215. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  216. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  217. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  218. package/src/utils/caniemail/get-css-functions.ts +25 -0
  219. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  220. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  221. package/src/utils/caniemail/get-css-unit.ts +3 -0
  222. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  223. package/src/utils/caniemail/get-element-names.ts +20 -0
  224. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  225. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +187 -0
  226. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  227. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  228. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  229. package/src/utils/cn.ts +6 -0
  230. package/src/utils/constants.ts +6 -0
  231. package/src/utils/contains-email-template.spec.ts +124 -0
  232. package/src/utils/contains-email-template.ts +33 -0
  233. package/src/utils/copy-text-to-clipboard.ts +7 -0
  234. package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
  235. package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
  236. package/src/utils/get-email-component.spec.ts +41 -0
  237. package/src/utils/get-email-component.ts +134 -0
  238. package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
  239. package/src/utils/get-emails-directory-metadata.ts +141 -0
  240. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  241. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  242. package/src/utils/improve-error-with-sourcemap.ts +85 -0
  243. package/src/utils/index.ts +6 -0
  244. package/src/utils/js-email-detection.spec.ts +24 -0
  245. package/src/utils/language-map.ts +7 -0
  246. package/src/utils/linting.ts +60 -0
  247. package/src/utils/load-stream.ts +15 -0
  248. package/src/utils/register-spinner-autostopping.ts +28 -0
  249. package/src/utils/result.ts +49 -0
  250. package/src/utils/run-bundled-code.ts +64 -0
  251. package/src/utils/sanitize.ts +6 -0
  252. package/src/utils/static-node-modules-for-vm.ts +93 -0
  253. package/src/utils/testing/js-email-export-default.js +17 -0
  254. package/src/utils/testing/js-email-test.js +18 -0
  255. package/src/utils/testing/mdx-email-test.js +128 -0
  256. package/src/utils/testing/request-response-email.tsx +9 -0
  257. package/src/utils/types/as.ts +26 -0
  258. package/src/utils/types/email-template.ts +8 -0
  259. package/src/utils/types/error-object.ts +11 -0
  260. package/src/utils/types/hot-reload-change.ts +13 -0
  261. package/src/utils/unreachable.ts +8 -0
  262. package/tailwind-internals.d.ts +133 -0
  263. package/tailwind.config.ts +99 -0
  264. package/tsconfig.json +45 -0
  265. 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
@@ -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,15 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html {
6
+ color-scheme: dark;
7
+ }
8
+
9
+ .popup-open iframe {
10
+ pointer-events: none;
11
+ }
12
+
13
+ nav > div > div > .line {
14
+ display: none;
15
+ }
@@ -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;
Binary file
@@ -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
+ };