@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,150 @@
1
+ import * as Collapsible from '@radix-ui/react-collapsible';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import type { ComponentProps } from 'react';
4
+ import { cn } from '../../utils';
5
+
6
+ export type ResultStatus = 'error' | 'warning' | 'success';
7
+
8
+ const statusStyles = {
9
+ error: 'text-red-600 hover:bg-red-600/10',
10
+ warning: 'text-yellow-300 hover:bg-yellow-400/10',
11
+ success: 'text-green-600 hover:bg-green-600/10',
12
+ };
13
+
14
+ interface ResultListProps {
15
+ status: ResultStatus;
16
+ label: React.ReactNode;
17
+
18
+ disabled?: boolean;
19
+ defaultOpen?: boolean;
20
+
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export const ResultList = ({
25
+ status,
26
+ label,
27
+
28
+ disabled,
29
+ defaultOpen,
30
+
31
+ children,
32
+ }: ResultListProps) => {
33
+ return (
34
+ <Collapsible.Root className="group" defaultOpen={defaultOpen && !disabled}>
35
+ <Collapsible.Trigger
36
+ className={cn(
37
+ 'group flex w-full items-center gap-1 rounded p-2 transition-colors duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
38
+ statusStyles[status],
39
+ disabled && 'cursor-not-allowed opacity-70',
40
+ )}
41
+ disabled={disabled}
42
+ >
43
+ <span
44
+ className={cn(
45
+ '-mt-[.125rem] transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
46
+ 'rotate-0 group-data-[state=open]:rotate-90',
47
+ )}
48
+ >
49
+ <svg
50
+ fill="none"
51
+ height="15"
52
+ viewBox="0 0 15 15"
53
+ width="15"
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ >
56
+ <path
57
+ clipRule="evenodd"
58
+ d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
59
+ fill="currentColor"
60
+ fillRule="evenodd"
61
+ />
62
+ </svg>
63
+ </span>
64
+ <div className="flex flex-1 items-center gap-1 font-bold text-[.625rem] uppercase tracking-wide">
65
+ {label}
66
+ </div>
67
+ </Collapsible.Trigger>
68
+ {children ? (
69
+ <Collapsible.Content>
70
+ <ol className="mt-2 mb-1 flex list-none flex-col gap-4">
71
+ {children}
72
+ </ol>
73
+ </Collapsible.Content>
74
+ ) : null}
75
+ </Collapsible.Root>
76
+ );
77
+ };
78
+
79
+ type ResultProps = {
80
+ status: ResultStatus;
81
+ } & ComponentProps<typeof motion.li>;
82
+
83
+ const resultAnimation = {
84
+ hidden: { opacity: 0, y: 10 },
85
+ visible: {
86
+ opacity: 1,
87
+ y: 0,
88
+ transition: { duration: 0.6, ease: 'easeOut', staggerChildren: 0.1 },
89
+ },
90
+ };
91
+
92
+ export const Result = ({ children, status, ...rest }: ResultProps) => {
93
+ return (
94
+ <AnimatePresence mode="wait">
95
+ <motion.li
96
+ data-status={status}
97
+ initial="hidden"
98
+ layout
99
+ variants={resultAnimation}
100
+ animate="visible"
101
+ {...rest}
102
+ className={cn(
103
+ 'group/item relative w-full rounded-md p-2 pl-4 transition-colors duration-300 ease-out hover:bg-slate-5',
104
+ rest.className,
105
+ )}
106
+ >
107
+ {children}
108
+ </motion.li>
109
+ </AnimatePresence>
110
+ );
111
+ };
112
+
113
+ const titleStatusAnimation = {
114
+ hidden: { opacity: 0, y: 5 },
115
+ visible: {
116
+ opacity: 1,
117
+ y: 0,
118
+ transition: { duration: 0.4, ease: 'easeOut' },
119
+ },
120
+ };
121
+
122
+ interface ResultStatusDescriptionProps {
123
+ children: React.ReactNode;
124
+ }
125
+
126
+ Result.StatusDescription = ({ children }: ResultStatusDescriptionProps) => {
127
+ return (
128
+ <motion.div
129
+ className="mt-1 font-semibold text-[.625rem] uppercase"
130
+ variants={titleStatusAnimation}
131
+ >
132
+ {children}
133
+ </motion.div>
134
+ );
135
+ };
136
+
137
+ interface ResultTitleProps {
138
+ children: React.ReactNode;
139
+ }
140
+
141
+ Result.Title = ({ children }: ResultTitleProps) => {
142
+ return (
143
+ <motion.div
144
+ className="flex w-full items-center gap-2 text-xs group-data-[status=error]/item:text-red-400 group-data-[status=success]/item:text-green-400 group-data-[status=warning]/item:text-yellow-300 "
145
+ variants={titleStatusAnimation}
146
+ >
147
+ {children}
148
+ </motion.div>
149
+ );
150
+ };
@@ -0,0 +1,40 @@
1
+ import Link from 'next/link';
2
+ import { useSearchParams } from 'next/navigation';
3
+
4
+ interface CodePreviewLineLinkProps {
5
+ line: number;
6
+ column: number;
7
+
8
+ type: 'react' | 'html';
9
+ }
10
+
11
+ export const CodePreviewLineLink = ({
12
+ line,
13
+ column,
14
+ type,
15
+ }: CodePreviewLineLinkProps) => {
16
+ const searchParams = useSearchParams();
17
+
18
+ const newSearchParams = new URLSearchParams(searchParams);
19
+ newSearchParams.set('view', 'source');
20
+ if (type === 'html') {
21
+ newSearchParams.set('lang', 'markup');
22
+ } else if (type === 'react') {
23
+ newSearchParams.set('lang', 'jsx');
24
+ }
25
+
26
+ const fragmentIdentifier = `#L${line}`;
27
+
28
+ return (
29
+ <Link
30
+ href={{
31
+ search: newSearchParams.toString(),
32
+ hash: fragmentIdentifier,
33
+ }}
34
+ scroll={false}
35
+ className="appearance-none underline mx-2"
36
+ >
37
+ L{line.toString().padStart(2, '0')}
38
+ </Link>
39
+ );
40
+ };
@@ -0,0 +1,113 @@
1
+ import { useRef, useState } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { nicenames } from '../../actions/email-validation/caniemail-data';
4
+ import {
5
+ type CompatibilityCheckingResult,
6
+ checkCompatibility,
7
+ } from '../../actions/email-validation/check-compatibility';
8
+ import { sanitize } from '../../utils';
9
+ import { loadStream } from '../../utils/load-stream';
10
+ import { IconWarning } from '../icons/icon-warning';
11
+ import { CodePreviewLineLink } from './code-preview-line-link';
12
+ import { Results } from './results';
13
+
14
+ export const useCompatibility = ({
15
+ reactMarkup,
16
+ emailPath,
17
+
18
+ initialResults,
19
+ }: {
20
+ reactMarkup: string;
21
+ emailPath: string;
22
+
23
+ initialResults?: CompatibilityCheckingResult[];
24
+ }) => {
25
+ const [results, setResults] = useState(initialResults);
26
+
27
+ const [loading, setLoading] = useState(false);
28
+ const isLoadingRef = useRef(false);
29
+
30
+ const load = async () => {
31
+ if (isLoadingRef.current) return;
32
+ isLoadingRef.current = true;
33
+ setLoading(true);
34
+
35
+ setResults([]);
36
+ let rawResults: CompatibilityCheckingResult[] = [];
37
+
38
+ try {
39
+ const stream = await checkCompatibility(reactMarkup, emailPath);
40
+ for await (const result of loadStream(stream)) {
41
+ if (result.status !== 'error') continue;
42
+ setResults((current) => {
43
+ if (!current) {
44
+ return [result];
45
+ }
46
+ rawResults = [...current, result];
47
+ return rawResults;
48
+ });
49
+ }
50
+ } catch (exception) {
51
+ console.error(exception);
52
+ toast.error(JSON.stringify(exception));
53
+ } finally {
54
+ setLoading(false);
55
+ isLoadingRef.current = false;
56
+ }
57
+
58
+ return rawResults;
59
+ };
60
+
61
+ return [results, { loading, load }] as const;
62
+ };
63
+
64
+ interface CompatibilityProps {
65
+ results: CompatibilityCheckingResult[] | undefined;
66
+ }
67
+
68
+ export const Compatibility = ({ results }: CompatibilityProps) => {
69
+ return (
70
+ <Results>
71
+ {results?.map((result, i) => {
72
+ const statsReportedNotWorking = Object.entries(
73
+ result.statsPerEmailClient,
74
+ ).filter(([, stats]) => stats.status === 'error');
75
+ const unsupportedClientsString = statsReportedNotWorking
76
+ .map(([emailClient]) => nicenames.family[emailClient])
77
+ .join(', ');
78
+
79
+ return (
80
+ <Results.Row key={i}>
81
+ <Results.Column>
82
+ <span className="flex text-red-400 uppercase gap-2 items-center">
83
+ <IconWarning />
84
+ {sanitize(result.entry.title)}
85
+ </span>
86
+ </Results.Column>
87
+ <Results.Column>
88
+ {statsReportedNotWorking.length > 0
89
+ ? `Not supported in ${unsupportedClientsString}`
90
+ : null}
91
+
92
+ <a
93
+ href={result.entry.url}
94
+ className="underline ml-2 decoration-slate-9 decoration-1 hover:decoration-slate-11 transition-colors hover:text-slate-12"
95
+ rel="noreferrer"
96
+ target="_blank"
97
+ >
98
+ More ↗
99
+ </a>
100
+ </Results.Column>
101
+ <Results.Column className="font-mono text-slate-11 text-right">
102
+ <CodePreviewLineLink
103
+ line={result.location.start.line}
104
+ column={result.location.start.column}
105
+ type="react"
106
+ />
107
+ </Results.Column>
108
+ </Results.Row>
109
+ );
110
+ })}
111
+ </Results>
112
+ );
113
+ };
@@ -0,0 +1,278 @@
1
+ import prettyBytes from 'pretty-bytes';
2
+ import { Children, useRef, useState } from 'react';
3
+ import type { ImageCheckingResult } from '../../actions/email-validation/check-images';
4
+ import type { LinkCheckingResult } from '../../actions/email-validation/check-links';
5
+ import { cn, sanitize } from '../../utils';
6
+ import { getLintingSources, loadLintingRowsFrom } from '../../utils/linting';
7
+ import { IconWarning } from '../icons/icon-warning';
8
+ import { CodePreviewLineLink } from './code-preview-line-link';
9
+ import { Results } from './results';
10
+
11
+ export type LintingRow =
12
+ | {
13
+ source: 'image';
14
+ result: ImageCheckingResult;
15
+ }
16
+ | {
17
+ source: 'link';
18
+ result: LinkCheckingResult;
19
+ };
20
+
21
+ interface LinterProps {
22
+ rows: LintingRow[] | undefined;
23
+ }
24
+
25
+ export const useLinter = ({
26
+ markup,
27
+
28
+ initialRows,
29
+ }: {
30
+ markup: string;
31
+
32
+ initialRows?: LintingRow[];
33
+ }) => {
34
+ const [rows, setRows] = useState<LintingRow[] | undefined>(initialRows);
35
+
36
+ const sources = getLintingSources(
37
+ markup,
38
+ 'location' in global
39
+ ? `${global.location.protocol}//${global.location.host}`
40
+ : '',
41
+ );
42
+
43
+ const [loading, setLoading] = useState(false);
44
+ const isStreaming = useRef(false);
45
+
46
+ const load = async () => {
47
+ if (isStreaming.current) return;
48
+ isStreaming.current = true;
49
+ setLoading(true);
50
+
51
+ setRows([]);
52
+ try {
53
+ let rows: LintingRow[] = [];
54
+ for await (const row of loadLintingRowsFrom(sources)) {
55
+ setRows((current) => {
56
+ if (!current) {
57
+ return [row];
58
+ }
59
+ const newArray = [...current, row];
60
+ newArray.sort((a, b) => {
61
+ if (a.result.status === 'error' && b.result.status === 'warning') {
62
+ return -1;
63
+ }
64
+
65
+ if (a.result.status === 'warning' && b.result.status === 'error') {
66
+ return 1;
67
+ }
68
+
69
+ return 0;
70
+ });
71
+ rows = newArray;
72
+ return newArray;
73
+ });
74
+ }
75
+ return rows;
76
+ } finally {
77
+ setLoading(false);
78
+ isStreaming.current = false;
79
+ }
80
+ };
81
+
82
+ return [rows, { loading, load }] as const;
83
+ };
84
+
85
+ export const Linter = ({ rows }: LinterProps) => {
86
+ if (rows === undefined) return null;
87
+
88
+ return (
89
+ <Results>
90
+ {rows.map((row, i) => {
91
+ if (row.source === 'link') {
92
+ const failingCheck = row.result.checks.find(
93
+ (check) => check.passed === false,
94
+ )!;
95
+ const metadata: React.ReactNode[] = [];
96
+ for (const check of row.result.checks) {
97
+ if (
98
+ check.type === 'fetch_attempt' &&
99
+ check.metadata.fetchStatusCode
100
+ ) {
101
+ metadata.push(<>HTTP {check.metadata.fetchStatusCode}</>);
102
+ }
103
+ }
104
+ metadata.push(
105
+ <CodePreviewLineLink
106
+ line={row.result.codeLocation.line}
107
+ column={row.result.codeLocation.column}
108
+ type="html"
109
+ />,
110
+ );
111
+ return (
112
+ <Result status={row.result.status} key={i}>
113
+ <Result.Name>{sanitize(failingCheck.type)}</Result.Name>
114
+ <Result.Description>
115
+ {failingCheck.type === 'security'
116
+ ? 'Insecure URL, use HTTPS instead of HTTP'
117
+ : null}
118
+ {failingCheck.type === 'fetch_attempt' &&
119
+ failingCheck.metadata.fetchStatusCode &&
120
+ failingCheck.metadata.fetchStatusCode >= 300 &&
121
+ failingCheck.metadata.fetchStatusCode < 400
122
+ ? 'There was a redirect, the content may have been moved'
123
+ : null}
124
+ {failingCheck.type === 'fetch_attempt' &&
125
+ failingCheck.metadata.fetchStatusCode &&
126
+ failingCheck.metadata.fetchStatusCode >= 400
127
+ ? 'The link is broken'
128
+ : null}
129
+ {failingCheck.type === 'fetch_attempt' &&
130
+ failingCheck.metadata.fetchStatusCode === undefined
131
+ ? 'The link could not be reached'
132
+ : null}
133
+ {failingCheck.type === 'syntax'
134
+ ? 'The link is broken due to invalid syntax'
135
+ : null}
136
+
137
+ <span className="ml-2 text-ellipsis overflow-hidden text-nowrap max-w-[30ch]">
138
+ {row.result.link}
139
+ </span>
140
+ </Result.Description>
141
+ <Result.Metadata>{metadata}</Result.Metadata>
142
+ </Result>
143
+ );
144
+ }
145
+
146
+ if (row.source === 'image') {
147
+ const failingCheck = row.result.checks.find(
148
+ (check) => check.passed === false,
149
+ )!;
150
+ const metadata: React.ReactNode[] = [];
151
+ for (const check of row.result.checks) {
152
+ if (check.type === 'image_size' && check.metadata.byteCount) {
153
+ metadata.push(prettyBytes(check.metadata.byteCount));
154
+ }
155
+ if (
156
+ check.type === 'fetch_attempt' &&
157
+ check.metadata.fetchStatusCode
158
+ ) {
159
+ metadata.push(<>HTTP {check.metadata.fetchStatusCode}</>);
160
+ }
161
+ }
162
+ metadata.push(
163
+ <CodePreviewLineLink
164
+ line={row.result.codeLocation.line}
165
+ column={row.result.codeLocation.column}
166
+ type="html"
167
+ />,
168
+ );
169
+ return (
170
+ <Result status={row.result.status} key={i}>
171
+ <Result.Name>{sanitize(failingCheck.type)}</Result.Name>
172
+ <Result.Description>
173
+ {failingCheck.type === 'security'
174
+ ? 'Insecure URL, use HTTPS instead of HTTP'
175
+ : null}
176
+ {failingCheck.type === 'fetch_attempt' &&
177
+ failingCheck.metadata.fetchStatusCode &&
178
+ failingCheck.metadata.fetchStatusCode >= 300 &&
179
+ failingCheck.metadata.fetchStatusCode < 400
180
+ ? 'There was a redirect, the image may have been moved'
181
+ : null}
182
+ {failingCheck.type === 'fetch_attempt' &&
183
+ failingCheck.metadata.fetchStatusCode &&
184
+ failingCheck.metadata.fetchStatusCode >= 400
185
+ ? 'The image is broken'
186
+ : null}
187
+ {failingCheck.type === 'fetch_attempt' &&
188
+ failingCheck.metadata.fetchStatusCode === undefined
189
+ ? 'The image could not be reached'
190
+ : null}
191
+ {failingCheck.type === 'syntax'
192
+ ? 'The image is broken due to an invalid source'
193
+ : null}
194
+
195
+ {failingCheck.type === 'accessibility'
196
+ ? 'Missing alt text'
197
+ : null}
198
+
199
+ {failingCheck.type === 'image_size' &&
200
+ failingCheck.metadata.byteCount
201
+ ? 'This image is too large, keep it under 1mb'
202
+ : null}
203
+
204
+ <span className="ml-2 text-ellipsis overflow-hidden text-nowrap max-w-[30ch]">
205
+ {row.result.source}
206
+ </span>
207
+ </Result.Description>
208
+ <Result.Metadata>{metadata}</Result.Metadata>
209
+ </Result>
210
+ );
211
+ }
212
+
213
+ return undefined;
214
+ })}
215
+ </Results>
216
+ );
217
+ };
218
+
219
+ interface ResultProps extends React.ComponentProps<typeof Results.Row> {
220
+ status: 'error' | 'warning' | 'success';
221
+ }
222
+
223
+ const Result = ({ children, className, status, ...props }: ResultProps) => {
224
+ return (
225
+ <Results.Row
226
+ data-status={status}
227
+ {...props}
228
+ className={cn('group/result', className)}
229
+ >
230
+ {children}
231
+ </Results.Row>
232
+ );
233
+ };
234
+
235
+ Result.Name = ({
236
+ children,
237
+ ...props
238
+ }: React.ComponentProps<typeof Results.Column>) => {
239
+ return (
240
+ <Results.Column {...props}>
241
+ <span className="flex uppercase gap-2 items-center group-data-[status=error]/result:text-red-400 group-data-[status=warning]/result:text-orange-300">
242
+ <IconWarning />
243
+ {typeof children === 'string' ? sanitize(children) : children}
244
+ </span>
245
+ </Results.Column>
246
+ );
247
+ };
248
+
249
+ Result.Description = ({
250
+ children,
251
+ className,
252
+ ...props
253
+ }: React.ComponentProps<typeof Results.Column>) => {
254
+ return <Results.Column {...props}>{children}</Results.Column>;
255
+ };
256
+
257
+ interface MetadatProps extends React.ComponentProps<typeof Results.Column> {
258
+ children: React.ReactNode[];
259
+ }
260
+
261
+ Result.Metadata = ({ children, className, ...props }: MetadatProps) => {
262
+ return (
263
+ <Results.Column
264
+ align="right"
265
+ {...props}
266
+ className={cn('font-mono text-slate-11', className)}
267
+ >
268
+ {Children.map(children, (child, index) => {
269
+ return (
270
+ <>
271
+ {index > 0 ? ' · ' : null}
272
+ {child}
273
+ </>
274
+ );
275
+ })}
276
+ </Results.Column>
277
+ );
278
+ };
File without changes
@@ -0,0 +1,51 @@
1
+ import { cn } from '../../utils';
2
+
3
+ export const Results = ({
4
+ children,
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<'table'>) => {
8
+ return (
9
+ <table
10
+ className={cn(
11
+ 'group relative w-full border-collapse text-left text-slate-10 text-sm',
12
+ className,
13
+ )}
14
+ >
15
+ <tbody>{children}</tbody>
16
+ </table>
17
+ );
18
+ };
19
+
20
+ Results.Row = ({
21
+ children,
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<'tr'>) => {
25
+ return (
26
+ <tr
27
+ className={cn(
28
+ 'border-collapse align-bottom border-slate-6 border-b last:border-b-0',
29
+ className,
30
+ )}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </tr>
35
+ );
36
+ };
37
+
38
+ Results.Column = ({
39
+ children,
40
+ className,
41
+ ...props
42
+ }: React.ComponentProps<'td'>) => {
43
+ return (
44
+ <td
45
+ className={cn('py-1.5 align-bottom font-regular', className)}
46
+ {...props}
47
+ >
48
+ {children}
49
+ </td>
50
+ );
51
+ };