react-email 4.0.0-alpha.4 → 4.0.0-alpha.6

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 (189) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/index.js +1175 -2658
  3. package/dist/cli/index.mjs +18 -12
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +31 -34
  6. package/dist/preview/.next/app-path-routes-manifest.json +6 -1
  7. package/dist/preview/.next/build-manifest.json +14 -14
  8. package/dist/preview/.next/cache/.rscinfo +1 -1
  9. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  13. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  14. package/dist/preview/.next/diagnostics/framework.json +1 -1
  15. package/dist/preview/.next/export-marker.json +6 -1
  16. package/dist/preview/.next/images-manifest.json +57 -1
  17. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  18. package/dist/preview/.next/next-server.js.nft.json +1 -1
  19. package/dist/preview/.next/prerender-manifest.json +41 -1
  20. package/dist/preview/.next/required-server-files.json +310 -1
  21. package/dist/preview/.next/routes-manifest.json +64 -1
  22. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  23. package/dist/preview/.next/server/app/_not-found/page.js.nft.json +1 -1
  24. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  25. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  26. package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  27. package/dist/preview/.next/server/app/page.js +1 -1
  28. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  29. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/dist/preview/.next/server/app/preview/[...slug]/page.js +47 -10
  31. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  32. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  33. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  34. package/dist/preview/.next/server/chunks/171.js +14 -0
  35. package/dist/preview/.next/server/chunks/446.js +6 -0
  36. package/dist/preview/.next/server/chunks/600.js +8 -0
  37. package/dist/preview/.next/server/chunks/811.js +13 -0
  38. package/dist/preview/.next/server/chunks/833.js +1 -0
  39. package/dist/preview/.next/server/functions-config-manifest.json +4 -1
  40. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  42. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  43. package/dist/preview/.next/server/pages/500.html +1 -1
  44. package/dist/preview/.next/server/pages/_app.js +1 -1
  45. package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
  46. package/dist/preview/.next/server/pages/_document.js +1 -1
  47. package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
  48. package/dist/preview/.next/server/pages/_error.js +1 -1
  49. package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
  50. package/dist/preview/.next/server/pages-manifest.json +5 -1
  51. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  52. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  53. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  54. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
  55. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
  56. package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
  57. package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
  58. package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
  59. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
  60. package/dist/preview/.next/static/chunks/{afa401a5-9ebf2515b1397993.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
  61. package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
  62. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
  63. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
  64. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
  65. package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
  66. package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
  67. package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
  68. package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
  69. package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
  70. package/dist/preview/.next/static/chunks/{webpack-9255716c9496e606.js → webpack-41e2667c9f086a4f.js} +1 -1
  71. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
  72. package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
  73. package/dist/preview/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
  74. package/dist/preview/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
  75. package/dist/preview/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
  76. package/dist/preview/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
  77. package/dist/preview/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
  78. package/dist/preview/.next/static/media/e4051546b3043204-s.p.otf +0 -0
  79. package/dist/preview/.next/trace +26 -22
  80. package/dist/preview/.next/types/cache-life.d.ts +3 -3
  81. package/package.json +17 -11
  82. package/scripts/build-preview-server.mjs +32 -0
  83. package/scripts/fill-caniemail-data.mjs +36 -0
  84. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  85. package/src/actions/email-validation/check-compatibility.ts +322 -0
  86. package/src/actions/email-validation/check-images.spec.tsx +21 -12
  87. package/src/actions/email-validation/check-images.ts +88 -86
  88. package/src/actions/email-validation/check-links.spec.tsx +24 -14
  89. package/src/actions/email-validation/check-links.ts +59 -56
  90. package/src/actions/get-email-path-from-slug.ts +1 -1
  91. package/src/actions/render-email-by-path.tsx +2 -1
  92. package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
  93. package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
  94. package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
  95. package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
  96. package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
  97. package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
  98. package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
  99. package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
  100. package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
  101. package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
  102. package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
  103. package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
  104. package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
  105. package/src/app/fonts.ts +39 -0
  106. package/src/app/layout.tsx +6 -3
  107. package/src/app/page.tsx +4 -4
  108. package/src/app/preview/[...slug]/page.tsx +73 -16
  109. package/src/app/preview/[...slug]/preview.tsx +49 -77
  110. package/src/components/code.tsx +0 -1
  111. package/src/components/icons/icon-base.tsx +4 -2
  112. package/src/components/icons/icon-reload.tsx +19 -0
  113. package/src/components/icons/icon-scanner.tsx +19 -0
  114. package/src/components/icons/icon-scissors.tsx +19 -0
  115. package/src/components/icons/icon-warning.tsx +31 -0
  116. package/src/components/send.tsx +1 -2
  117. package/src/components/shell.tsx +52 -88
  118. package/src/components/sidebar/file-tree-directory-children.tsx +1 -1
  119. package/src/components/sidebar/file-tree.tsx +1 -1
  120. package/src/components/sidebar/sidebar.tsx +23 -378
  121. package/src/components/toolbar/linter.tsx +310 -0
  122. package/src/components/toolbar/results-table.tsx +0 -0
  123. package/src/components/toolbar/results.tsx +48 -0
  124. package/src/components/toolbar/spam-assassin.tsx +144 -0
  125. package/src/components/toolbar/toolbar-button.tsx +50 -0
  126. package/src/components/toolbar/use-cached-state.ts +33 -0
  127. package/src/components/toolbar.tsx +197 -0
  128. package/src/components/tooltip-content.tsx +1 -2
  129. package/src/components/topbar/view-size-controls.tsx +1 -0
  130. package/src/components/topbar.tsx +29 -48
  131. package/src/contexts/emails.tsx +2 -1
  132. package/src/contexts/preview.tsx +81 -0
  133. package/src/hooks/use-email-rendering-result.ts +2 -1
  134. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  135. package/src/utils/caniemail/all-css-properties.ts +358 -0
  136. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  137. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  138. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  139. package/src/utils/caniemail/get-css-functions.ts +25 -0
  140. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  141. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  142. package/src/utils/caniemail/get-css-unit.ts +3 -0
  143. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  144. package/src/utils/caniemail/get-element-names.ts +20 -0
  145. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  146. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
  147. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  148. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  149. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  150. package/src/utils/get-email-component.ts +34 -67
  151. package/src/utils/linting.ts +85 -0
  152. package/src/utils/result.ts +49 -0
  153. package/src/utils/run-bundled-code.ts +64 -0
  154. package/tailwind-internals.d.ts +133 -0
  155. package/tailwind.config.ts +1 -0
  156. package/tsconfig.json +9 -3
  157. package/build-preview-server.mjs +0 -25
  158. package/dist/preview/.next/server/chunks/196.js +0 -5
  159. package/dist/preview/.next/server/chunks/300.js +0 -13
  160. package/dist/preview/.next/server/chunks/631.js +0 -6
  161. package/dist/preview/.next/server/chunks/644.js +0 -1
  162. package/dist/preview/.next/server/chunks/734.js +0 -15
  163. package/dist/preview/.next/static/Pt6wqIrWnQxbiyqaKNFOx/_buildManifest.js +0 -1
  164. package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +0 -1
  165. package/dist/preview/.next/static/chunks/447-886131c35ca42b91.js +0 -1
  166. package/dist/preview/.next/static/chunks/490-d5745684930d49e0.js +0 -1
  167. package/dist/preview/.next/static/chunks/5fec7a0a-5179023f3f5a9421.js +0 -1
  168. package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +0 -1
  169. package/dist/preview/.next/static/chunks/797-46f6c20952f0a280.js +0 -2
  170. package/dist/preview/.next/static/chunks/app/_not-found/page-96d3eac723be3ee2.js +0 -1
  171. package/dist/preview/.next/static/chunks/app/layout-d06046b8a368df3b.js +0 -1
  172. package/dist/preview/.next/static/chunks/app/page-ef1c23b954fbd0b5.js +0 -1
  173. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-ea8e1ae2b5a4a0ec.js +0 -1
  174. package/dist/preview/.next/static/chunks/framework-e7cae9cecd5c9ba2.js +0 -1
  175. package/dist/preview/.next/static/chunks/main-app-9f2fb5ea26e2765b.js +0 -1
  176. package/dist/preview/.next/static/chunks/main-df761fde212f9cda.js +0 -1
  177. package/dist/preview/.next/static/chunks/pages/_app-203a61b355820ccf.js +0 -1
  178. package/dist/preview/.next/static/chunks/pages/_error-1764ca54938748c8.js +0 -1
  179. package/dist/preview/.next/static/css/e4822d5ba3082a95.css +0 -3
  180. package/dist/preview/.next/static/css/ec5d7e66bd3b6cb8.css +0 -1
  181. package/src/app/inter.ts +0 -7
  182. package/src/components/icons/icon-circle-check.tsx +0 -21
  183. package/src/components/icons/icon-circle-close.tsx +0 -17
  184. package/src/components/icons/icon-circle-warning.tsx +0 -17
  185. package/src/components/sidebar/image-checker.tsx +0 -162
  186. package/src/components/sidebar/link-checker.tsx +0 -151
  187. package/src/components/sidebar/spam-assassin.tsx +0 -158
  188. /package/dist/preview/.next/static/{Pt6wqIrWnQxbiyqaKNFOx → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
  189. /package/src/components/{sidebar → toolbar}/checking-results.tsx +0 -0
@@ -3,7 +3,7 @@
3
3
  import { parse } from 'node-html-parser';
4
4
  import { quickFetch } from './quick-fetch';
5
5
 
6
- type Check = { passed: boolean } & (
6
+ export type LinkCheck = { passed: boolean } & (
7
7
  | {
8
8
  type: 'fetch_attempt';
9
9
  metadata: {
@@ -21,71 +21,74 @@ type Check = { passed: boolean } & (
21
21
  export interface LinkCheckingResult {
22
22
  status: 'success' | 'warning' | 'error';
23
23
  link: string;
24
- checks: Check[];
24
+ checks: LinkCheck[];
25
25
  }
26
26
 
27
27
  export const checkLinks = async (code: string) => {
28
28
  const ast = parse(code);
29
29
 
30
- const linkCheckingResults: LinkCheckingResult[] = [];
30
+ const readableStream = new ReadableStream<LinkCheckingResult>({
31
+ async start(controller) {
32
+ const anchors = ast.querySelectorAll('a');
33
+ for await (const anchor of anchors) {
34
+ const link = anchor.attributes.href;
35
+ if (!link) continue;
36
+ if (link.startsWith('mailto:')) continue;
31
37
 
32
- const anchors = ast.querySelectorAll('a');
33
- for await (const anchor of anchors) {
34
- const link = anchor.attributes.href;
35
- if (!link) continue;
36
- if (linkCheckingResults.some((result) => result.link === link)) continue;
37
- if (link.startsWith('mailto:')) continue;
38
+ const result: LinkCheckingResult = {
39
+ link,
40
+ status: 'success',
41
+ checks: [],
42
+ };
38
43
 
39
- const result: LinkCheckingResult = {
40
- link,
41
- status: 'success',
42
- checks: [],
43
- };
44
+ try {
45
+ const url = new URL(link);
46
+ result.checks.push({
47
+ passed: true,
48
+ type: 'syntax',
49
+ });
44
50
 
45
- try {
46
- const url = new URL(link);
47
- result.checks.push({
48
- passed: true,
49
- type: 'syntax',
50
- });
51
+ if (link.startsWith('http://')) {
52
+ result.checks.push({
53
+ passed: false,
54
+ type: 'security',
55
+ });
56
+ result.status = 'warning';
57
+ } else {
58
+ result.checks.push({
59
+ passed: true,
60
+ type: 'security',
61
+ });
62
+ }
51
63
 
52
- if (link.startsWith('https://')) {
53
- result.checks.push({
54
- passed: true,
55
- type: 'security',
56
- });
57
- } else {
58
- result.checks.push({
59
- passed: false,
60
- type: 'security',
61
- });
62
- result.status = 'warning';
63
- }
64
+ const res = await quickFetch(url);
65
+ const hasSucceeded =
66
+ res.statusCode?.toString().startsWith('2') ?? false;
67
+ result.checks.push({
68
+ type: 'fetch_attempt',
69
+ passed: hasSucceeded,
70
+ metadata: {
71
+ fetchStatusCode: res.statusCode,
72
+ },
73
+ });
74
+ if (!hasSucceeded) {
75
+ result.status = res.statusCode?.toString().startsWith('3')
76
+ ? 'warning'
77
+ : 'error';
78
+ }
79
+ } catch (exception) {
80
+ result.checks.push({
81
+ passed: false,
82
+ type: 'syntax',
83
+ });
84
+ result.status = 'error';
85
+ }
64
86
 
65
- const res = await quickFetch(url);
66
- const hasSucceeded = res.statusCode?.toString().startsWith('2') ?? false;
67
- result.checks.push({
68
- type: 'fetch_attempt',
69
- passed: hasSucceeded,
70
- metadata: {
71
- fetchStatusCode: res.statusCode,
72
- },
73
- });
74
- if (!hasSucceeded) {
75
- result.status = res.statusCode?.toString().startsWith('3')
76
- ? 'warning'
77
- : 'error';
87
+ controller.enqueue(result);
78
88
  }
79
- } catch (exception) {
80
- result.checks.push({
81
- passed: false,
82
- type: 'syntax',
83
- });
84
- result.status = 'error';
85
- }
86
-
87
- linkCheckingResults.push(result);
88
- }
89
+ controller.close();
90
+ },
91
+ });
89
92
 
90
- return linkCheckingResults;
93
+ return readableStream;
91
94
  };
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { cache } from 'react';
5
- import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
5
+ import { emailsDirectoryAbsolutePath } from '../app/env';
6
6
 
7
7
  // eslint-disable-next-line @typescript-eslint/require-await
8
8
  export const getEmailPathFromSlug = cache(async (slug: string) => {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import logSymbols from 'log-symbols';
6
6
  import ora from 'ora';
7
+ import { isBuilding } from '../app/env';
7
8
  import { getEmailComponent } from '../utils/get-email-component';
8
9
  import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
9
10
  import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';
@@ -35,7 +36,7 @@ export const renderEmailByPath = async (
35
36
 
36
37
  const emailFilename = path.basename(emailPath);
37
38
  let spinner: ora.Ora | undefined;
38
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
39
+ if (!isBuilding) {
39
40
  spinner = ora({
40
41
  text: `Rendering email template ${emailFilename}\n`,
41
42
  prefixText: ' ',
@@ -8,3 +8,5 @@ export const userProjectLocation = process.env.USER_PROJECT_LOCATION!;
8
8
  /** ONLY ACCESSIBLE ON THE SERVER */
9
9
  export const emailsDirectoryAbsolutePath =
10
10
  process.env.EMAILS_DIR_ABSOLUTE_PATH!;
11
+
12
+ export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
@@ -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
+ });
@@ -1,9 +1,9 @@
1
1
  import type { Metadata } from 'next';
2
2
  import './globals.css';
3
3
  import { EmailsProvider } from '../contexts/emails';
4
- import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
5
4
  import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata';
6
- import { inter } from './inter';
5
+ import { emailsDirectoryAbsolutePath } from './env';
6
+ import { inter, sfMono } from './fonts';
7
7
 
8
8
  export const metadata: Metadata = {
9
9
  title: 'React Email',
@@ -23,7 +23,10 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
23
23
  }
24
24
 
25
25
  return (
26
- <html className={inter.className} lang="en">
26
+ <html
27
+ className={`${inter.variable} ${sfMono.variable} font-sans`}
28
+ lang="en"
29
+ >
27
30
  <body className="relative flex h-screen flex-col overflow-x-hidden bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
28
31
  <EmailsProvider
29
32
  initialEmailsDirectoryMetadata={emailsDirectoryMetadata}
package/src/app/page.tsx CHANGED
@@ -3,8 +3,8 @@ import Image from 'next/image';
3
3
  import Link from 'next/link';
4
4
  import { Button, Heading, Text } from '../components';
5
5
  import CodeSnippet from '../components/code-snippet';
6
- import { Shell } from '../components/shell';
7
- import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
6
+ import { Shell, ShellContent } from '../components/shell';
7
+ import { emailsDirectoryAbsolutePath } from './env';
8
8
  import logo from './logo.png';
9
9
 
10
10
  const Home = () => {
@@ -12,7 +12,7 @@ const Home = () => {
12
12
 
13
13
  return (
14
14
  <Shell>
15
- <div className="relative mx-auto flex h-[inherit] max-w-lg items-center justify-center p-8">
15
+ <ShellContent className="mx-auto flex max-w-lg items-center justify-center p-8">
16
16
  <div className="-mt-10 relative flex flex-col items-center gap-3 text-center">
17
17
  <Image
18
18
  alt="React Email Icon"
@@ -38,7 +38,7 @@ const Home = () => {
38
38
  <Link href="https://react.email/docs">Check the docs</Link>
39
39
  </Button>
40
40
  </div>
41
- </div>
41
+ </ShellContent>
42
42
  </Shell>
43
43
  );
44
44
  };
@@ -3,8 +3,14 @@ import { redirect } from 'next/navigation';
3
3
  import { Suspense } from 'react';
4
4
  import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
5
5
  import { renderEmailByPath } from '../../../actions/render-email-by-path';
6
- import { emailsDirectoryAbsolutePath } from '../../../utils/emails-directory-absolute-path';
6
+ import { Shell } from '../../../components/shell';
7
+ import { Toolbar } from '../../../components/toolbar';
8
+ import type { LintingRow } from '../../../components/toolbar/linter';
9
+ import type { SpamCheckingResult } from '../../../components/toolbar/spam-assassin';
10
+ import { PreviewProvider } from '../../../contexts/preview';
7
11
  import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
12
+ import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
13
+ import { emailsDirectoryAbsolutePath, isBuilding } from '../../env';
8
14
  import Home from '../../page';
9
15
  import Preview from './preview';
10
16
 
@@ -50,27 +56,78 @@ This is most likely not an issue with the preview server. Maybe there was a typo
50
56
 
51
57
  const serverEmailRenderingResult = await renderEmailByPath(emailPath);
52
58
 
53
- if (
54
- process.env.NEXT_PUBLIC_IS_BUILDING === 'true' &&
55
- 'error' in serverEmailRenderingResult
56
- ) {
59
+ if (isBuilding && 'error' in serverEmailRenderingResult) {
57
60
  throw new Error(serverEmailRenderingResult.error.message, {
58
61
  cause: serverEmailRenderingResult.error,
59
62
  });
60
63
  }
61
64
 
65
+ let spamCheckingResult: SpamCheckingResult | undefined = undefined;
66
+ let lintingRows: LintingRow[] | undefined = undefined;
67
+
68
+ if (isBuilding && !('error' in serverEmailRenderingResult)) {
69
+ const lintingSources = getLintingSources(
70
+ serverEmailRenderingResult.markup,
71
+ serverEmailRenderingResult.reactMarkup,
72
+ emailPath,
73
+ '',
74
+ );
75
+ lintingRows = [];
76
+ for await (const row of loadLintingRowsFrom(lintingSources)) {
77
+ lintingRows.push(row);
78
+ }
79
+ lintingRows.sort((a, b) => {
80
+ if (a.result.status === 'error' && b.result.status === 'warning') {
81
+ return -1;
82
+ }
83
+
84
+ if (a.result.status === 'warning' && b.result.status === 'error') {
85
+ return 1;
86
+ }
87
+
88
+ return 0;
89
+ });
90
+
91
+ const response = await fetch('https://react.email/api/check-spam', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({
95
+ html: serverEmailRenderingResult.markup,
96
+ plainText: serverEmailRenderingResult.plainText,
97
+ }),
98
+ });
99
+ const responseBody = (await response.json()) as
100
+ | { error: string }
101
+ | SpamCheckingResult;
102
+ if ('error' in responseBody) {
103
+ throw new Error(`Failed doing Spam Check. ${responseBody.error}`, {
104
+ cause: responseBody,
105
+ });
106
+ }
107
+
108
+ spamCheckingResult = responseBody;
109
+ }
110
+
62
111
  return (
63
- // This suspense is so that this page doesn't throw warnings
64
- // on the build of the preview server de-opting into
65
- // client-side rendering on build
66
- <Suspense fallback={<Home />}>
67
- <Preview
68
- emailPath={emailPath}
69
- pathSeparator={path.sep}
70
- serverRenderingResult={serverEmailRenderingResult}
71
- slug={slug}
72
- />
73
- </Suspense>
112
+ <PreviewProvider
113
+ emailSlug={slug}
114
+ emailPath={emailPath}
115
+ serverRenderingResult={serverEmailRenderingResult}
116
+ >
117
+ <Shell currentEmailOpenSlug={slug}>
118
+ {/* This suspense is so that this page doesn't throw warnings */}
119
+ {/* on the build of the preview server de-opting into */}
120
+ {/* client-side rendering on build */}
121
+ <Suspense fallback={<Home />}>
122
+ <Preview emailTitle={path.basename(emailPath)} />
123
+
124
+ <Toolbar
125
+ serverLintingRows={lintingRows}
126
+ serverSpamCheckingResult={spamCheckingResult}
127
+ />
128
+ </Suspense>
129
+ </Shell>
130
+ </PreviewProvider>
74
131
  );
75
132
  };
76
133
 
@@ -1,37 +1,32 @@
1
1
  'use client';
2
2
 
3
3
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
- import { useState } from 'react';
4
+ import { use, useState } from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { Toaster } from 'sonner';
7
7
  import { useDebouncedCallback } from 'use-debounce';
8
- import type { EmailRenderingResult } from '../../../actions/render-email-by-path';
8
+ import { Topbar } from '../../../components';
9
9
  import { CodeContainer } from '../../../components/code-container';
10
10
  import {
11
11
  ResizableWarpper,
12
12
  makeIframeDocumentBubbleEvents,
13
13
  } from '../../../components/resizable-wrapper';
14
- import { Shell } from '../../../components/shell';
14
+ import { Send } from '../../../components/send';
15
+ import { ShellContent } from '../../../components/shell';
15
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';
16
20
  import { useClampedState } from '../../../hooks/use-clamped-state';
17
- import { useEmailRenderingResult } from '../../../hooks/use-email-rendering-result';
18
- import { useHotreload } from '../../../hooks/use-hot-reload';
19
- import { useRenderingMetadata } from '../../../hooks/use-rendering-metadata';
20
21
  import { RenderingError } from './rendering-error';
21
22
 
22
23
  interface PreviewProps {
23
- slug: string;
24
- emailPath: string;
25
- pathSeparator: string;
26
- serverRenderingResult: EmailRenderingResult;
24
+ emailTitle: string;
27
25
  }
28
26
 
29
- const Preview = ({
30
- slug,
31
- emailPath,
32
- pathSeparator,
33
- serverRenderingResult,
34
- }: PreviewProps) => {
27
+ const Preview = ({ emailTitle }: PreviewProps) => {
28
+ const { renderingResult, renderedEmailMetadata } = use(PreviewContext)!;
29
+
35
30
  const router = useRouter();
36
31
  const pathname = usePathname();
37
32
  const searchParams = useSearchParams();
@@ -39,34 +34,6 @@ const Preview = ({
39
34
  const activeView = searchParams.get('view') ?? 'preview';
40
35
  const activeLang = searchParams.get('lang') ?? 'jsx';
41
36
 
42
- const renderingResult = useEmailRenderingResult(
43
- emailPath,
44
- serverRenderingResult,
45
- );
46
-
47
- const renderedEmailMetadata = useRenderingMetadata(
48
- emailPath,
49
- renderingResult,
50
- serverRenderingResult,
51
- );
52
-
53
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
54
- // this will not change on runtime so it doesn't violate
55
- // the rules of hooks
56
- // eslint-disable-next-line react-hooks/rules-of-hooks
57
- useHotreload((changes) => {
58
- const changeForThisEmail = changes.find((change) =>
59
- change.filename.includes(slug),
60
- );
61
-
62
- if (typeof changeForThisEmail !== 'undefined') {
63
- if (changeForThisEmail.event === 'unlink') {
64
- router.push('/');
65
- }
66
- }
67
- });
68
- }
69
-
70
37
  const handleViewChange = (view: string) => {
71
38
  const params = new URLSearchParams(searchParams);
72
39
  params.set('view', view);
@@ -80,7 +47,8 @@ const Preview = ({
80
47
  router.push(`${pathname}?${params.toString()}`);
81
48
  };
82
49
 
83
- const hasNoErrors = typeof renderedEmailMetadata !== 'undefined';
50
+ const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
51
+ const hasErrors = 'error' in renderingResult;
84
52
 
85
53
  const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
86
54
  const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
@@ -107,31 +75,37 @@ const Preview = ({
107
75
  }, 300);
108
76
 
109
77
  return (
110
- <Shell
111
- activeView={activeView}
112
- currentEmailOpenSlug={slug}
113
- markup={renderedEmailMetadata?.markup}
114
- plainText={renderedEmailMetadata?.plainText}
115
- pathSeparator={pathSeparator}
116
- setActiveView={handleViewChange}
117
- setViewHeight={(height) => {
118
- setHeight(height);
119
- flushSync(() => {
120
- handleSaveViewSize();
121
- });
122
- }}
123
- setViewWidth={(width) => {
124
- setWidth(width);
125
- flushSync(() => {
126
- handleSaveViewSize();
127
- });
128
- }}
129
- viewHeight={height}
130
- viewWidth={width}
131
- >
132
- {/* This relative is so that when there is any error the user can still switch between emails */}
133
- <div
134
- className="relative flex h-full bg-gray-200 pb-8"
78
+ <>
79
+ <Topbar emailTitle={emailTitle}>
80
+ <ViewSizeControls
81
+ setViewHeight={(height) => {
82
+ setHeight(height);
83
+ flushSync(() => {
84
+ handleSaveViewSize();
85
+ });
86
+ }}
87
+ setViewWidth={(width) => {
88
+ setWidth(width);
89
+ flushSync(() => {
90
+ handleSaveViewSize();
91
+ });
92
+ }}
93
+ viewHeight={height}
94
+ viewWidth={width}
95
+ />
96
+ <ActiveViewToggleGroup
97
+ activeView={activeView}
98
+ setActiveView={handleViewChange}
99
+ />
100
+ {hasRenderingMetadata ? (
101
+ <div className="flex justify-end">
102
+ <Send markup={renderedEmailMetadata.markup} />
103
+ </div>
104
+ ) : null}
105
+ </Topbar>
106
+
107
+ <ShellContent
108
+ className="relative flex bg-gray-200"
135
109
  ref={(element) => {
136
110
  const observer = new ResizeObserver((entry) => {
137
111
  const [elementEntry] = entry;
@@ -150,11 +124,9 @@ const Preview = ({
150
124
  };
151
125
  }}
152
126
  >
153
- {'error' in renderingResult ? (
154
- <RenderingError error={renderingResult.error} />
155
- ) : null}
127
+ {hasErrors ? <RenderingError error={renderingResult.error} /> : null}
156
128
 
157
- {hasNoErrors ? (
129
+ {hasRenderingMetadata ? (
158
130
  <>
159
131
  {activeView === 'preview' && (
160
132
  <ResizableWarpper
@@ -189,7 +161,7 @@ const Preview = ({
189
161
  width: `${width}px`,
190
162
  height: `${height}px`,
191
163
  }}
192
- title={slug}
164
+ title={emailTitle}
193
165
  />
194
166
  </ResizableWarpper>
195
167
  )}
@@ -224,8 +196,8 @@ const Preview = ({
224
196
  ) : null}
225
197
 
226
198
  <Toaster />
227
- </div>
228
- </Shell>
199
+ </ShellContent>
200
+ </>
229
201
  );
230
202
  };
231
203
 
@@ -78,7 +78,6 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
78
78
  {line.map((token, key) => {
79
79
  const tokenProps = getTokenProps({
80
80
  token,
81
- key,
82
81
  });
83
82
  const isException =
84
83
  token.content === 'from' &&
@@ -8,7 +8,7 @@ export interface IconProps extends RootProps {
8
8
  }
9
9
 
10
10
  export const IconBase = React.forwardRef<IconElement, Readonly<IconProps>>(
11
- ({ size = 20, ...props }, forwardedRef) => (
11
+ ({ size = 20, children, ...props }, forwardedRef) => (
12
12
  <svg
13
13
  fill="none"
14
14
  height={size}
@@ -17,7 +17,9 @@ export const IconBase = React.forwardRef<IconElement, Readonly<IconProps>>(
17
17
  width={size}
18
18
  xmlns="http://www.w3.org/2000/svg"
19
19
  {...props}
20
- />
20
+ >
21
+ {children}
22
+ </svg>
21
23
  ),
22
24
  );
23
25
 
@@ -0,0 +1,19 @@
1
+ export const IconReload = (props: React.ComponentProps<'svg'>) => {
2
+ return (
3
+ <svg
4
+ width="12"
5
+ height="12"
6
+ viewBox="0 0 12 12"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ {...props}
10
+ >
11
+ <path
12
+ fillRule="evenodd"
13
+ clipRule="evenodd"
14
+ d="M10.52 6C10.52 3.73168 8.75221 1.48 6.00006 1.48C3.77741 1.48 2.67886 3.1251 2.21074 3.99999H3.60005C3.82096 3.99999 4.00005 4.17908 4.00005 4.39999C4.00005 4.6209 3.82096 4.79999 3.60005 4.79999H1.20005C0.979137 4.79999 0.800049 4.6209 0.800049 4.39999V1.99999C0.800049 1.77908 0.979137 1.59999 1.20005 1.59999C1.42096 1.59999 1.60005 1.77908 1.60005 1.99999V3.45056C2.16367 2.45702 3.4673 0.679993 6.00006 0.679993C9.25029 0.679993 11.32 3.34831 11.32 6C11.32 8.65169 9.25029 11.32 6.00006 11.32C4.44499 11.32 3.15027 10.7047 2.22843 9.76673C1.73486 9.26449 1.34939 8.67121 1.08658 8.03257C1.0025 7.8283 1.09995 7.59453 1.30424 7.51046C1.50853 7.42638 1.7423 7.52384 1.82637 7.72812C2.05104 8.27401 2.38001 8.77961 2.79901 9.20593C3.57646 9.99705 4.66802 10.52 6.00006 10.52C8.75221 10.52 10.52 8.26833 10.52 6Z"
15
+ fill="currentColor"
16
+ />
17
+ </svg>
18
+ );
19
+ };