@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
@@ -0,0 +1,124 @@
1
+ import {
2
+ containsEmailTemplate,
3
+ removeFilenameExtension,
4
+ } from './contains-email-template';
5
+ import type { EmailsDirectory } from './get-emails-directory-metadata';
6
+
7
+ describe('removeFilenameExtension()', () => {
8
+ it('should work with a single .', () => {
9
+ expect(removeFilenameExtension('email-template.tsx')).toBe(
10
+ 'email-template',
11
+ );
12
+ });
13
+
14
+ it('should work with an example test file', () => {
15
+ expect(removeFilenameExtension('email-template.spec.tsx')).toBe(
16
+ 'email-template.spec',
17
+ );
18
+ });
19
+
20
+ it('should do nothing when there is no extension', () => {
21
+ expect(removeFilenameExtension('email-template')).toBe('email-template');
22
+ });
23
+ });
24
+
25
+ describe('containsEmailTemplate()', () => {
26
+ const directory: EmailsDirectory = {
27
+ absolutePath: '/fake/path/emails',
28
+ directoryName: 'emails',
29
+ relativePath: '',
30
+ emailFilenames: [],
31
+ subDirectories: [
32
+ {
33
+ absolutePath: '/fake/path/emails/magic-links',
34
+ directoryName: 'magic-links',
35
+ relativePath: 'magic-links',
36
+ emailFilenames: [
37
+ 'aws-verify-email',
38
+ 'linear-login-code',
39
+ 'notion-magic-link',
40
+ 'plaid-verify-identity',
41
+ 'raycast-magic-link',
42
+ 'slack-confirm',
43
+ ],
44
+ subDirectories: [
45
+ {
46
+ absolutePath: '/fake/path/emails/magic-links/resend',
47
+ directoryName: 'resend',
48
+ emailFilenames: ['verify-email'],
49
+ relativePath: 'magic-links/resend',
50
+ subDirectories: [],
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ absolutePath: '/fake/path/emails/newsletters',
56
+ directoryName: 'newsletters',
57
+ relativePath: 'newsletters',
58
+ emailFilenames: [
59
+ 'codepen-challengers',
60
+ 'google-play-policy-update',
61
+ 'stack-overflow-tips',
62
+ ],
63
+ subDirectories: [],
64
+ },
65
+ {
66
+ absolutePath: '/fake/path/emails/notifications',
67
+ directoryName: 'notifications',
68
+ relativePath: 'notifications',
69
+ emailFilenames: [
70
+ 'github-access-token',
71
+ 'papermark-year-in-review',
72
+ 'vercel-invite-user',
73
+ 'yelp-recent-login',
74
+ ],
75
+ subDirectories: [],
76
+ },
77
+ {
78
+ absolutePath: '/fake/path/emails/receipts',
79
+ directoryName: 'receipts',
80
+ relativePath: 'receipts',
81
+ emailFilenames: ['apple-receipt', 'nike-receipt'],
82
+ subDirectories: [],
83
+ },
84
+ {
85
+ absolutePath: '/fake/path/emails/reset-password',
86
+ directoryName: 'reset-password',
87
+ relativePath: 'reset-password',
88
+ emailFilenames: ['dropbox-reset-password', 'twitch-reset-password'],
89
+ subDirectories: [],
90
+ },
91
+ {
92
+ absolutePath: '/fake/path/emails/reviews',
93
+ directoryName: 'reviews',
94
+ relativePath: 'reviews',
95
+ emailFilenames: ['airbnb-review', 'amazon-review'],
96
+ subDirectories: [],
97
+ },
98
+ {
99
+ absolutePath: '/fake/path/emails/welcome',
100
+ directoryName: 'welcome',
101
+ relativePath: 'welcome',
102
+ emailFilenames: ['koala-welcome', 'netlify-welcome', 'stripe-welcome'],
103
+ subDirectories: [],
104
+ },
105
+ ],
106
+ };
107
+ it('should work with email inside a single sub directory', () => {
108
+ expect(containsEmailTemplate('welcome/koala-welcome', directory)).toBe(
109
+ true,
110
+ );
111
+ expect(containsEmailTemplate('welcome/missing-template', directory)).toBe(
112
+ false,
113
+ );
114
+ });
115
+
116
+ it('should work with email inside a second sub directory', () => {
117
+ expect(
118
+ containsEmailTemplate('magic-links/resend/verify-email', directory),
119
+ ).toBe(true);
120
+ expect(
121
+ containsEmailTemplate('magic-links/resend/missing-template', directory),
122
+ ).toBe(false);
123
+ });
124
+ });
@@ -0,0 +1,33 @@
1
+ import type { EmailsDirectory } from './get-emails-directory-metadata';
2
+
3
+ export const removeFilenameExtension = (filename: string): string => {
4
+ const parts = filename.split('.');
5
+
6
+ if (parts.length > 1) {
7
+ return parts.slice(0, -1).join('.');
8
+ }
9
+
10
+ return filename;
11
+ };
12
+
13
+ export const containsEmailTemplate = (
14
+ relativeEmailPath: string,
15
+ directory: EmailsDirectory,
16
+ ) => {
17
+ const remainingSegments = relativeEmailPath
18
+ .replace(directory.relativePath, '')
19
+ .split('/')
20
+ .filter(Boolean);
21
+ if (remainingSegments.length === 1) {
22
+ const emailFilename = removeFilenameExtension(remainingSegments[0]!);
23
+ return directory.emailFilenames.includes(emailFilename);
24
+ }
25
+ const subDirectory = directory.subDirectories.find(
26
+ (sub) => sub.directoryName === remainingSegments[0],
27
+ );
28
+ if (subDirectory === undefined) {
29
+ return false;
30
+ }
31
+
32
+ return containsEmailTemplate(relativeEmailPath, subDirectory);
33
+ };
@@ -0,0 +1,7 @@
1
+ export const copyTextToClipboard = async (text: string) => {
2
+ try {
3
+ await navigator.clipboard.writeText(text);
4
+ } catch {
5
+ throw new Error('Not able to copy');
6
+ }
7
+ };
@@ -0,0 +1,3 @@
1
+ export function escapeStringForRegex(string: string) {
2
+ return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
3
+ }
@@ -0,0 +1,63 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { Loader, PluginBuild, ResolveOptions } from 'esbuild';
4
+ import { escapeStringForRegex } from './escape-string-for-regex';
5
+
6
+ /**
7
+ * Made to export the `render` function out of the user's email template
8
+ * so that issues like https://github.com/resend/react-email/issues/649 don't
9
+ * happen.
10
+ *
11
+ * This also exports the `createElement` from the user's React version as well
12
+ * to avoid mismatches.
13
+ *
14
+ * This avoids multiple versions of React being involved, i.e., the version
15
+ * in the CLI vs. the version the user has on their emails.
16
+ */
17
+ export const renderingUtilitiesExporter = (emailTemplates: string[]) => ({
18
+ name: 'rendering-utilities-exporter',
19
+ setup: (b: PluginBuild) => {
20
+ b.onLoad(
21
+ {
22
+ filter: new RegExp(
23
+ emailTemplates
24
+ .map((emailPath) => escapeStringForRegex(emailPath))
25
+ .join('|'),
26
+ ),
27
+ },
28
+ async ({ path: pathToFile }) => {
29
+ return {
30
+ contents: `${await fs.readFile(pathToFile, 'utf8')};
31
+ export { render } from 'react-email-module-that-will-export-render'
32
+ export { createElement as reactEmailCreateReactElement } from 'react';
33
+ `,
34
+ loader: path.extname(pathToFile).slice(1) as Loader,
35
+ };
36
+ },
37
+ );
38
+
39
+ b.onResolve(
40
+ { filter: /^react-email-module-that-will-export-render$/ },
41
+ async (args) => {
42
+ const options: ResolveOptions = {
43
+ kind: 'import-statement',
44
+ importer: args.importer,
45
+ resolveDir: args.resolveDir,
46
+ namespace: args.namespace,
47
+ };
48
+ let result = await b.resolve('@react-email/render', options);
49
+ if (result.errors.length === 0) {
50
+ return result;
51
+ }
52
+
53
+ // If @react-email/render does not exist, resolve to @react-email/components
54
+ result = await b.resolve('@react-email/components', options);
55
+ if (result.errors.length > 0 && result.errors[0]) {
56
+ result.errors[0].text =
57
+ "Failed trying to import `render` from either `@react-email/render` or `@react-email/components` to be able to render your email template.\n Maybe you don't have either of them installed?";
58
+ }
59
+ return result;
60
+ },
61
+ );
62
+ },
63
+ });
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ import { getEmailComponent } from './get-email-component';
3
+
4
+ describe('getEmailComponent()', () => {
5
+ describe('Node internals support', () => {
6
+ test('Request', async () => {
7
+ const result = await getEmailComponent(
8
+ path.resolve(__dirname, './testing/request-response-email.tsx'),
9
+ );
10
+ if ('error' in result) {
11
+ console.log(result.error);
12
+ expect('error' in result, 'there should be no errors').toBe(false);
13
+ }
14
+ });
15
+ });
16
+
17
+ test('with a demo email template', async () => {
18
+ const result = await getEmailComponent(
19
+ path.resolve(
20
+ __dirname,
21
+ '../../../../apps/demo/emails/notifications/vercel-invite-user.tsx',
22
+ ),
23
+ );
24
+
25
+ if ('error' in result) {
26
+ console.log(result.error);
27
+ expect('error' in result).toBe(false);
28
+ } else {
29
+ expect(result.emailComponent).toBeTruthy();
30
+ expect(result.sourceMapToOriginalFile).toBeTruthy();
31
+
32
+ const emailHtml = await result.render(
33
+ result.createElement(
34
+ result.emailComponent,
35
+ result.emailComponent.PreviewProps,
36
+ ),
37
+ );
38
+ expect(emailHtml).toMatchSnapshot();
39
+ }
40
+ });
41
+ });
@@ -0,0 +1,134 @@
1
+ import path from 'node:path';
2
+ import type { render } from '@react-email/components';
3
+ import { type BuildFailure, build, type OutputFile } from 'esbuild';
4
+ import type React from 'react';
5
+ import type { RawSourceMap } from 'source-map-js';
6
+ import { z } from 'zod';
7
+ import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
8
+ import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
9
+ import { isErr } from './result';
10
+ import { runBundledCode } from './run-bundled-code';
11
+ import type { EmailTemplate as EmailComponent } from './types/email-template';
12
+ import type { ErrorObject } from './types/error-object';
13
+
14
+ const EmailComponentModule = z.object({
15
+ default: z.any(),
16
+ render: z.function(),
17
+ reactEmailCreateReactElement: z.function(),
18
+ });
19
+
20
+ export const getEmailComponent = async (
21
+ emailPath: string,
22
+ ): Promise<
23
+ | {
24
+ emailComponent: EmailComponent;
25
+
26
+ createElement: typeof React.createElement;
27
+
28
+ render: typeof render;
29
+
30
+ sourceMapToOriginalFile: RawSourceMap;
31
+ }
32
+ | { error: ErrorObject }
33
+ > => {
34
+ let outputFiles: OutputFile[];
35
+ try {
36
+ const buildData = await build({
37
+ bundle: true,
38
+ entryPoints: [emailPath],
39
+ plugins: [renderingUtilitiesExporter([emailPath])],
40
+ platform: 'node',
41
+ write: false,
42
+
43
+ format: 'cjs',
44
+ jsx: 'automatic',
45
+ logLevel: 'silent',
46
+ // allows for using jsx on a .js file
47
+ loader: {
48
+ '.js': 'jsx',
49
+ },
50
+ outdir: 'stdout', // just a stub for esbuild, it won't actually write to this folder
51
+ sourcemap: 'external',
52
+ });
53
+ outputFiles = buildData.outputFiles;
54
+ } catch (exception) {
55
+ const buildFailure = exception as BuildFailure;
56
+ return {
57
+ error: {
58
+ message: buildFailure.message,
59
+ stack: buildFailure.stack,
60
+ name: buildFailure.name,
61
+ cause: buildFailure.cause,
62
+ },
63
+ };
64
+ }
65
+
66
+ const sourceMapFile = outputFiles[0]!;
67
+ const bundledEmailFile = outputFiles[1]!;
68
+ const builtEmailCode = bundledEmailFile.text;
69
+
70
+ const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
71
+ // because it will have a path like <tsconfigLocation>/stdout/email.js.map
72
+ sourceMapToEmail.sourceRoot = path.resolve(sourceMapFile.path, '../..');
73
+ sourceMapToEmail.sources = sourceMapToEmail.sources.map((source) =>
74
+ path.resolve(sourceMapFile.path, '..', source),
75
+ );
76
+
77
+ const runningResult = runBundledCode(builtEmailCode, emailPath);
78
+
79
+ if (isErr(runningResult)) {
80
+ const { error } = runningResult;
81
+ if (error instanceof Error) {
82
+ error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
83
+
84
+ return {
85
+ error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
86
+ };
87
+ }
88
+
89
+ throw error;
90
+ }
91
+
92
+ const parseResult = EmailComponentModule.safeParse(runningResult.value);
93
+
94
+ if (parseResult.error) {
95
+ return {
96
+ error: improveErrorWithSourceMap(
97
+ new Error(
98
+ `The email component at ${emailPath} does not contain the expected exports`,
99
+ {
100
+ cause: parseResult.error,
101
+ },
102
+ ),
103
+ emailPath,
104
+ sourceMapToEmail,
105
+ ),
106
+ };
107
+ }
108
+
109
+ if (typeof parseResult.data.default !== 'function') {
110
+ return {
111
+ error: improveErrorWithSourceMap(
112
+ new Error(
113
+ `The email component at ${emailPath} does not contain a default exported function`,
114
+ {
115
+ cause: parseResult.error,
116
+ },
117
+ ),
118
+ emailPath,
119
+ sourceMapToEmail,
120
+ ),
121
+ };
122
+ }
123
+
124
+ const { data: componentModule } = parseResult;
125
+
126
+ return {
127
+ emailComponent: componentModule.default as EmailComponent,
128
+ render: componentModule.render as typeof render,
129
+ createElement:
130
+ componentModule.reactEmailCreateReactElement as typeof React.createElement,
131
+
132
+ sourceMapToOriginalFile: sourceMapToEmail,
133
+ };
134
+ };
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import { getEmailsDirectoryMetadata } from './get-emails-directory-metadata';
3
+
4
+ test('getEmailsDirectoryMetadata on demo emails', async () => {
5
+ const emailsDirectoryPath = path.resolve(
6
+ __dirname,
7
+ '../../../../apps/demo/emails',
8
+ );
9
+ expect(await getEmailsDirectoryMetadata(emailsDirectoryPath)).toEqual({
10
+ absolutePath: emailsDirectoryPath,
11
+ directoryName: 'emails',
12
+ relativePath: '',
13
+ emailFilenames: [],
14
+ subDirectories: [
15
+ {
16
+ absolutePath: `${emailsDirectoryPath}/magic-links`,
17
+ directoryName: 'magic-links',
18
+ relativePath: 'magic-links',
19
+ emailFilenames: [
20
+ 'aws-verify-email',
21
+ 'linear-login-code',
22
+ 'notion-magic-link',
23
+ 'plaid-verify-identity',
24
+ 'raycast-magic-link',
25
+ 'slack-confirm',
26
+ ],
27
+ subDirectories: [],
28
+ },
29
+ {
30
+ absolutePath: `${emailsDirectoryPath}/newsletters`,
31
+ directoryName: 'newsletters',
32
+ relativePath: 'newsletters',
33
+ emailFilenames: [
34
+ 'codepen-challengers',
35
+ 'google-play-policy-update',
36
+ 'stack-overflow-tips',
37
+ ],
38
+ subDirectories: [],
39
+ },
40
+ {
41
+ absolutePath: `${emailsDirectoryPath}/notifications`,
42
+ directoryName: 'notifications',
43
+ relativePath: 'notifications',
44
+ emailFilenames: [
45
+ 'github-access-token',
46
+ 'papermark-year-in-review',
47
+ 'vercel-invite-user',
48
+ 'yelp-recent-login',
49
+ ],
50
+ subDirectories: [],
51
+ },
52
+ {
53
+ absolutePath: `${emailsDirectoryPath}/receipts`,
54
+ directoryName: 'receipts',
55
+ relativePath: 'receipts',
56
+ emailFilenames: ['apple-receipt', 'nike-receipt'],
57
+ subDirectories: [],
58
+ },
59
+ {
60
+ absolutePath: `${emailsDirectoryPath}/reset-password`,
61
+ directoryName: 'reset-password',
62
+ relativePath: 'reset-password',
63
+ emailFilenames: ['dropbox-reset-password', 'twitch-reset-password'],
64
+ subDirectories: [],
65
+ },
66
+ {
67
+ absolutePath: `${emailsDirectoryPath}/reviews`,
68
+ directoryName: 'reviews',
69
+ relativePath: 'reviews',
70
+ emailFilenames: ['airbnb-review', 'amazon-review'],
71
+ subDirectories: [],
72
+ },
73
+ {
74
+ absolutePath: `${emailsDirectoryPath}/welcome`,
75
+ directoryName: 'welcome',
76
+ relativePath: 'welcome',
77
+ emailFilenames: ['koala-welcome', 'netlify-welcome', 'stripe-welcome'],
78
+ subDirectories: [],
79
+ },
80
+ ],
81
+ });
82
+ });
@@ -0,0 +1,141 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const isFileAnEmail = async (fullPath: string): Promise<boolean> => {
6
+ let fileHandle: fs.promises.FileHandle;
7
+ try {
8
+ fileHandle = await fs.promises.open(fullPath, 'r');
9
+ } catch (exception) {
10
+ console.warn(exception);
11
+ return false;
12
+ }
13
+ const stat = await fileHandle.stat();
14
+
15
+ if (stat.isDirectory()) {
16
+ await fileHandle.close();
17
+ return false;
18
+ }
19
+
20
+ const { ext } = path.parse(fullPath);
21
+
22
+ if (!['.js', '.tsx', '.jsx'].includes(ext)) {
23
+ await fileHandle.close();
24
+ return false;
25
+ }
26
+
27
+ // check with a heuristic to see if the file has at least
28
+ // a default export (ES6) or module.exports (CommonJS) or named exports (MDX)
29
+ const fileContents = await fileHandle.readFile('utf8');
30
+
31
+ await fileHandle.close();
32
+
33
+ // Check for ES6 export default syntax
34
+ const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
35
+
36
+ // Check for CommonJS module.exports syntax
37
+ const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
38
+
39
+ // Check for named exports (used in MDX files) and ensure at least one is marked as default
40
+ const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(
41
+ fileContents,
42
+ );
43
+
44
+ return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
45
+ };
46
+
47
+ export interface EmailsDirectory {
48
+ absolutePath: string;
49
+ relativePath: string;
50
+ directoryName: string;
51
+ emailFilenames: string[];
52
+ subDirectories: EmailsDirectory[];
53
+ }
54
+
55
+ const mergeDirectoriesWithSubDirectories = (
56
+ emailsDirectoryMetadata: EmailsDirectory,
57
+ ): EmailsDirectory => {
58
+ let currentResultingMergedDirectory: EmailsDirectory =
59
+ emailsDirectoryMetadata;
60
+
61
+ while (
62
+ currentResultingMergedDirectory.emailFilenames.length === 0 &&
63
+ currentResultingMergedDirectory.subDirectories.length === 1
64
+ ) {
65
+ const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0]!;
66
+ currentResultingMergedDirectory = {
67
+ ...onlySubDirectory,
68
+ directoryName: path.join(
69
+ currentResultingMergedDirectory.directoryName,
70
+ onlySubDirectory.directoryName,
71
+ ),
72
+ };
73
+ }
74
+
75
+ return currentResultingMergedDirectory;
76
+ };
77
+
78
+ export const getEmailsDirectoryMetadata = async (
79
+ absolutePathToEmailsDirectory: string,
80
+ keepFileExtensions = false,
81
+ isSubDirectory = false,
82
+
83
+ baseDirectoryPath = absolutePathToEmailsDirectory,
84
+ ): Promise<EmailsDirectory | undefined> => {
85
+ if (!fs.existsSync(absolutePathToEmailsDirectory)) return;
86
+
87
+ const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, {
88
+ withFileTypes: true,
89
+ });
90
+
91
+ const isEmailPredicates = await Promise.all(
92
+ dirents.map((dirent) =>
93
+ isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name)),
94
+ ),
95
+ );
96
+ const emailFilenames = dirents
97
+ .filter((_, i) => isEmailPredicates[i])
98
+ .map((dirent) =>
99
+ keepFileExtensions
100
+ ? dirent.name
101
+ : dirent.name.replace(path.extname(dirent.name), ''),
102
+ );
103
+
104
+ const subDirectories = await Promise.all(
105
+ dirents
106
+ .filter(
107
+ (dirent) =>
108
+ dirent.isDirectory() &&
109
+ !dirent.name.startsWith('_') &&
110
+ dirent.name !== 'static',
111
+ )
112
+ .map((dirent) => {
113
+ const direntAbsolutePath = path.join(
114
+ absolutePathToEmailsDirectory,
115
+ dirent.name,
116
+ );
117
+
118
+ return getEmailsDirectoryMetadata(
119
+ direntAbsolutePath,
120
+ keepFileExtensions,
121
+ true,
122
+ baseDirectoryPath,
123
+ ) as Promise<EmailsDirectory>;
124
+ }),
125
+ );
126
+
127
+ const emailsMetadata = {
128
+ absolutePath: absolutePathToEmailsDirectory,
129
+ relativePath: path.relative(
130
+ baseDirectoryPath,
131
+ absolutePathToEmailsDirectory,
132
+ ),
133
+ directoryName: absolutePathToEmailsDirectory.split(path.sep).pop()!,
134
+ emailFilenames,
135
+ subDirectories,
136
+ } satisfies EmailsDirectory;
137
+
138
+ return isSubDirectory
139
+ ? mergeDirectoriesWithSubDirectories(emailsMetadata)
140
+ : emailsMetadata;
141
+ };
@@ -0,0 +1,11 @@
1
+ import { getLineAndColumnFromOffset } from './get-line-and-column-from-offset';
2
+
3
+ test('getLineAndColumnFromOffset()', () => {
4
+ const content = `export default function MyEmail() {
5
+ return <div className="testing classes to make sure this is not removed" id="my-div" aria-label="my beautiful div">
6
+ inside the div, should also stay unchanged
7
+ </div>;
8
+ }`;
9
+ const offset = content.indexOf('className');
10
+ expect(getLineAndColumnFromOffset(offset, content)).toEqual([2, 15]);
11
+ });
@@ -0,0 +1,11 @@
1
+ export const getLineAndColumnFromOffset = (
2
+ offset: number,
3
+ content: string,
4
+ ): [line: number, column: number] => {
5
+ const lineBreaks = [...content.slice(0, offset).matchAll(/\n|\r|\r\n/g)];
6
+
7
+ const line = lineBreaks.length + 1;
8
+ const column = offset - (lineBreaks[lineBreaks.length - 1]?.index ?? 0);
9
+
10
+ return [line, column];
11
+ };