react-email 4.0.0-alpha.5 → 4.0.0-alpha.7

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 (203) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/index.js +1179 -2659
  3. package/dist/cli/index.mjs +17 -11
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +32 -31
  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 +51 -11
  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/446.js +6 -0
  35. package/dist/preview/.next/server/chunks/600.js +8 -0
  36. package/dist/preview/.next/server/chunks/811.js +13 -0
  37. package/dist/preview/.next/server/chunks/816.js +14 -0
  38. package/dist/preview/.next/server/chunks/943.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/Pms2orsQgT5xpttCfZfH5/_buildManifest.js +1 -0
  55. package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
  56. package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
  57. package/dist/preview/.next/static/chunks/683-1fb40795502f6e63.js +1 -0
  58. package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
  59. package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
  60. package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
  61. package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
  62. package/dist/preview/.next/static/chunks/{afa401a5-a600c227dacf3ab4.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
  63. package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
  64. package/dist/preview/.next/static/chunks/app/layout-ffdee5cc1be30e7b.js +1 -0
  65. package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
  66. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9e22979a25c836c0.js +1 -0
  67. package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
  68. package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
  69. package/dist/preview/.next/static/chunks/main-app-256b213b179a95cc.js +1 -0
  70. package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
  71. package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
  72. package/dist/preview/.next/static/chunks/{webpack-2eb145a20ee6cb77.js → webpack-41e2667c9f086a4f.js} +1 -1
  73. package/dist/preview/.next/static/css/eaae8ce545b295f9.css +3 -0
  74. package/dist/preview/.next/trace +26 -22
  75. package/dist/preview/.next/types/app/layout.ts +1 -1
  76. package/dist/preview/.next/types/app/page.ts +84 -0
  77. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  78. package/dist/preview/.next/types/cache-life.d.ts +3 -3
  79. package/package.json +14 -9
  80. package/scripts/build-preview-server.mjs +32 -0
  81. package/scripts/fill-caniemail-data.mjs +36 -0
  82. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  83. package/src/actions/email-validation/check-compatibility.ts +321 -0
  84. package/src/actions/email-validation/check-images.spec.tsx +15 -13
  85. package/src/actions/email-validation/check-images.ts +8 -2
  86. package/src/actions/email-validation/check-links.spec.tsx +27 -15
  87. package/src/actions/email-validation/check-links.ts +8 -2
  88. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  89. package/src/actions/get-email-path-from-slug.ts +1 -1
  90. package/src/actions/render-email-by-path.tsx +2 -1
  91. package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +5 -0
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/page.tsx +1 -1
  94. package/src/app/preview/[...slug]/page.tsx +89 -19
  95. package/src/app/preview/[...slug]/preview.tsx +25 -68
  96. package/src/components/code-container.tsx +90 -71
  97. package/src/components/code.tsx +106 -43
  98. package/src/components/icons/icon-info.tsx +18 -0
  99. package/src/components/icons/icon-reload.tsx +13 -14
  100. package/src/components/logo.tsx +3 -2
  101. package/src/components/resizable-wrapper.tsx +1 -4
  102. package/src/components/sidebar/file-tree-directory-children.tsx +1 -0
  103. package/src/components/sidebar/sidebar.tsx +2 -3
  104. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  105. package/src/components/toolbar/compatibility.tsx +113 -0
  106. package/src/components/toolbar/linter.tsx +226 -125
  107. package/src/components/toolbar/results.tsx +5 -2
  108. package/src/components/toolbar/spam-assassin.tsx +40 -43
  109. package/src/components/toolbar/toolbar-button.tsx +52 -0
  110. package/src/components/toolbar/use-cached-state.ts +33 -0
  111. package/src/components/toolbar.tsx +196 -110
  112. package/src/components/tooltip-content.tsx +1 -1
  113. package/src/components/topbar/view-size-controls.tsx +1 -1
  114. package/src/components/topbar.tsx +4 -29
  115. package/src/contexts/emails.tsx +2 -1
  116. package/src/contexts/fragment-identifier.tsx +46 -0
  117. package/src/contexts/preview.tsx +81 -0
  118. package/src/hooks/use-email-rendering-result.ts +2 -1
  119. package/src/hooks/use-fragment-identifier.ts +14 -0
  120. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  121. package/src/utils/caniemail/all-css-properties.ts +358 -0
  122. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  123. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  124. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  125. package/src/utils/caniemail/get-css-functions.ts +25 -0
  126. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  127. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  128. package/src/utils/caniemail/get-css-unit.ts +3 -0
  129. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  130. package/src/utils/caniemail/get-element-names.ts +20 -0
  131. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  132. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +203 -0
  133. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  134. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  135. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  136. package/src/utils/get-email-component.ts +34 -67
  137. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  138. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  139. package/src/utils/index.ts +1 -0
  140. package/src/utils/linting.ts +60 -0
  141. package/src/utils/load-stream.ts +15 -0
  142. package/src/utils/result.ts +49 -0
  143. package/src/utils/run-bundled-code.ts +64 -0
  144. package/src/utils/sanitize.ts +6 -0
  145. package/tailwind-internals.d.ts +133 -0
  146. package/tsconfig.json +9 -3
  147. package/build-preview-server.mjs +0 -25
  148. package/dist/preview/.next/cache/images/TcyzHbFXGFjrOu3wEMvDoSmqCh3qP3iiNqJf0QbED9Y/60.1741728556140.cQ5qicbpvoXZ7leVmWqG2ElLwXB1ynYeSv8MBSA-QeM.Vy8iMWM3MGUtMTk1ODcxYmIyNzMi.webp +0 -0
  149. package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
  150. package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
  151. package/dist/preview/.next/cache/webpack/client-development/10.pack.gz +0 -0
  152. package/dist/preview/.next/cache/webpack/client-development/11.pack.gz +0 -0
  153. package/dist/preview/.next/cache/webpack/client-development/12.pack.gz +0 -0
  154. package/dist/preview/.next/cache/webpack/client-development/13.pack.gz +0 -0
  155. package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
  156. package/dist/preview/.next/cache/webpack/client-development/3.pack.gz +0 -0
  157. package/dist/preview/.next/cache/webpack/client-development/4.pack.gz +0 -0
  158. package/dist/preview/.next/cache/webpack/client-development/5.pack.gz +0 -0
  159. package/dist/preview/.next/cache/webpack/client-development/6.pack.gz +0 -0
  160. package/dist/preview/.next/cache/webpack/client-development/7.pack.gz +0 -0
  161. package/dist/preview/.next/cache/webpack/client-development/8.pack.gz +0 -0
  162. package/dist/preview/.next/cache/webpack/client-development/9.pack.gz +0 -0
  163. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
  164. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  165. package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
  166. package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
  167. package/dist/preview/.next/cache/webpack/server-development/2.pack.gz +0 -0
  168. package/dist/preview/.next/cache/webpack/server-development/3.pack.gz +0 -0
  169. package/dist/preview/.next/cache/webpack/server-development/4.pack.gz +0 -0
  170. package/dist/preview/.next/cache/webpack/server-development/5.pack.gz +0 -0
  171. package/dist/preview/.next/cache/webpack/server-development/6.pack.gz +0 -0
  172. package/dist/preview/.next/cache/webpack/server-development/7.pack.gz +0 -0
  173. package/dist/preview/.next/cache/webpack/server-development/8.pack.gz +0 -0
  174. package/dist/preview/.next/cache/webpack/server-development/9.pack.gz +0 -0
  175. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
  176. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  177. package/dist/preview/.next/server/chunks/143.js +0 -6
  178. package/dist/preview/.next/server/chunks/409.js +0 -5
  179. package/dist/preview/.next/server/chunks/46.js +0 -1
  180. package/dist/preview/.next/server/chunks/478.js +0 -14
  181. package/dist/preview/.next/server/chunks/707.js +0 -13
  182. package/dist/preview/.next/static/B4EYZiVzdylEG9lAIl-aO/_buildManifest.js +0 -1
  183. package/dist/preview/.next/static/chunks/575-bc52750855c25df4.js +0 -2
  184. package/dist/preview/.next/static/chunks/684-0f1ef7361c499798.js +0 -1
  185. package/dist/preview/.next/static/chunks/684c6b30-0c65da32762fc4ee.js +0 -1
  186. package/dist/preview/.next/static/chunks/81-e7539b08d9d3fb4d.js +0 -1
  187. package/dist/preview/.next/static/chunks/883-70c8267c50bc4133.js +0 -1
  188. package/dist/preview/.next/static/chunks/921-d1dc8c63f49e85d6.js +0 -1
  189. package/dist/preview/.next/static/chunks/app/_not-found/page-03ce767859c36d4e.js +0 -1
  190. package/dist/preview/.next/static/chunks/app/layout-7cf14e28880544f1.js +0 -1
  191. package/dist/preview/.next/static/chunks/app/page-065cb49b0a078541.js +0 -1
  192. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-656510fd180c803c.js +0 -1
  193. package/dist/preview/.next/static/chunks/framework-2a724981073c3a29.js +0 -1
  194. package/dist/preview/.next/static/chunks/main-552b9719bbc3a274.js +0 -1
  195. package/dist/preview/.next/static/chunks/main-app-914a73336fd45af5.js +0 -1
  196. package/dist/preview/.next/static/chunks/pages/_app-77ca34bce25ac75c.js +0 -1
  197. package/dist/preview/.next/static/chunks/pages/_error-73f611c46abbb495.js +0 -1
  198. package/dist/preview/.next/static/css/2df96d9ee014e8de.css +0 -3
  199. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
  200. package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
  201. package/src/components/icons/icon-scanner.tsx +0 -19
  202. package/src/components/icons/icon-scissors.tsx +0 -19
  203. /package/dist/preview/.next/static/{B4EYZiVzdylEG9lAIl-aO → Pms2orsQgT5xpttCfZfH5}/_ssgManifest.js +0 -0
@@ -0,0 +1,321 @@
1
+ 'use server';
2
+ import { parse } from '@babel/parser';
3
+ import traverse from '@babel/traverse';
4
+ import {
5
+ type SourceLocation,
6
+ convertLocationIntoObject,
7
+ getObjectVariables,
8
+ } from '../../utils/caniemail/ast/get-object-variables';
9
+ import type { StylePropertyUsage } from '../../utils/caniemail/ast/get-used-style-properties';
10
+ import {
11
+ doesPropertyHaveLocation,
12
+ getUsedStyleProperties,
13
+ } from '../../utils/caniemail/ast/get-used-style-properties';
14
+ import type {
15
+ CompatibilityStats,
16
+ SupportStatus,
17
+ } from '../../utils/caniemail/get-compatibility-stats-for-entry';
18
+ import { getCompatibilityStatsForEntry } from '../../utils/caniemail/get-compatibility-stats-for-entry';
19
+ import { getCssFunctions } from '../../utils/caniemail/get-css-functions';
20
+ import { getCssPropertyNames } from '../../utils/caniemail/get-css-property-names';
21
+ import { getCssPropertyWithValue } from '../../utils/caniemail/get-css-property-with-value';
22
+ import { getCssUnit } from '../../utils/caniemail/get-css-unit';
23
+ import { getElementAttributes } from '../../utils/caniemail/get-element-attributes';
24
+ import { getElementNames } from '../../utils/caniemail/get-element-names';
25
+ import { supportEntries } from './caniemail-data';
26
+
27
+ export interface CompatibilityCheckingResult {
28
+ location: SourceLocation;
29
+ source: string;
30
+ entry: SupportEntry;
31
+ status: SupportStatus;
32
+ statsPerEmailClient: CompatibilityStats['perEmailClient'];
33
+ }
34
+
35
+ export type EmailClient =
36
+ | 'gmail'
37
+ | 'outlook'
38
+ | 'yahoo'
39
+ | 'apple-mail'
40
+ | 'aol'
41
+ | 'thunderbird'
42
+ | 'microsoft'
43
+ | 'samsung-email'
44
+ | 'sfr'
45
+ | 'orange'
46
+ | 'protonmail'
47
+ | 'hey'
48
+ | 'mail-ru'
49
+ | 'fastmail'
50
+ | 'laposte'
51
+ | 't-online-de'
52
+ | 'free-fr'
53
+ | 'gmx'
54
+ | 'web-de'
55
+ | 'ionos-1and1'
56
+ | 'rainloop'
57
+ | 'wp-pl';
58
+
59
+ export type Platform =
60
+ | 'desktop-app'
61
+ | 'desktop-webmail'
62
+ | 'mobile-webmail'
63
+ | 'webmail'
64
+ | 'ios'
65
+ | 'android'
66
+ | 'windows'
67
+ | 'macos'
68
+ | 'windows-mail'
69
+ | 'outlook-com';
70
+
71
+ export type SupportEntryCategroy = 'html' | 'css' | 'image' | 'others';
72
+
73
+ export interface SupportEntry {
74
+ slug: string;
75
+ title: string;
76
+ description: string | null;
77
+ url: string;
78
+ category: SupportEntryCategroy;
79
+ tags: string[];
80
+ keywords: string | null;
81
+ last_test_date: string;
82
+ test_url: string;
83
+ test_results_url: string | null;
84
+ stats: Partial<
85
+ Record<
86
+ EmailClient,
87
+ Partial<
88
+ Record<
89
+ Platform,
90
+ /*
91
+ This last Record<string, string> has only one key, as the
92
+ ordered version of caniemail's data is meant to be something like:
93
+
94
+ [
95
+ { "1.0": "u" },
96
+ { "2.0": "y" },
97
+ { "3.0": "p #1" },
98
+ ]
99
+
100
+ So only one key for each object inside of this array, TypeScript can't really
101
+ describe this though AFAIK.
102
+ */
103
+ Record</* version */ string, string>[]
104
+ >
105
+ >
106
+ >
107
+ >;
108
+ notes: string | null;
109
+ notes_by_num: Record<number, string> | null;
110
+ }
111
+
112
+ const relevantEmailClients: EmailClient[] = [
113
+ 'gmail',
114
+ 'apple-mail',
115
+ 'outlook',
116
+ 'yahoo',
117
+ ];
118
+
119
+ export const checkCompatibility = async (
120
+ reactCode: string,
121
+ emailPath: string,
122
+ ) => {
123
+ const ast = parse(reactCode, {
124
+ strictMode: false,
125
+ errorRecovery: true,
126
+ sourceType: 'unambiguous',
127
+ plugins: ['jsx', 'typescript', 'decorators'],
128
+ });
129
+
130
+ const getSourceCodeAt = (location: SourceLocation) => {
131
+ const codeLines = reactCode.split(/\n|\r|\r\n/);
132
+ const source = codeLines
133
+ .slice(
134
+ Math.max(location.start.line - 2, 0),
135
+ Math.min(location.end.line + 2, codeLines.length),
136
+ )
137
+ .join('\n');
138
+ return source;
139
+ };
140
+
141
+ const objectVariables = getObjectVariables(ast);
142
+ const usedStyleProperties = await getUsedStyleProperties(
143
+ ast,
144
+ reactCode,
145
+ emailPath,
146
+ objectVariables,
147
+ );
148
+ const readableStream = new ReadableStream<CompatibilityCheckingResult>({
149
+ async start(controller) {
150
+ for (const entry of supportEntries) {
151
+ const compatibilityStats = getCompatibilityStatsForEntry(
152
+ entry,
153
+ relevantEmailClients,
154
+ );
155
+ if (Object.keys(compatibilityStats.perEmailClient).length === 0)
156
+ continue;
157
+ if (compatibilityStats.status === 'success') continue;
158
+
159
+ if (entry.category === 'html') {
160
+ const entryElements = getElementNames(entry.title, entry.keywords);
161
+ const entryAttributes = getElementAttributes(entry.title);
162
+ const htmlEntryType = (() => {
163
+ if (entryElements.length > 0) {
164
+ return 'element';
165
+ }
166
+
167
+ if (entryAttributes.length > 0) {
168
+ return 'attribute';
169
+ }
170
+ })();
171
+
172
+ if (!htmlEntryType) continue;
173
+
174
+ let addedInsight = false;
175
+ if (htmlEntryType === 'element') {
176
+ traverse(ast, {
177
+ JSXOpeningElement(path) {
178
+ if (path.node.name.type === 'JSXIdentifier' && !addedInsight) {
179
+ const elementName = path.node.name.name;
180
+ if (
181
+ entryElements.includes(elementName) &&
182
+ path.node.name.loc
183
+ ) {
184
+ addedInsight = true;
185
+ controller.enqueue({
186
+ entry,
187
+ source: getSourceCodeAt(path.node.name.loc),
188
+ location: convertLocationIntoObject(path.node.name.loc),
189
+ statsPerEmailClient: compatibilityStats.perEmailClient,
190
+ status: compatibilityStats.status,
191
+ });
192
+ }
193
+ }
194
+ },
195
+ });
196
+ } else {
197
+ traverse(ast, {
198
+ JSXAttribute(path) {
199
+ if (path.node.name.type === 'JSXIdentifier' && !addedInsight) {
200
+ const attributeName = path.node.name.name;
201
+ if (
202
+ entryAttributes.includes(attributeName) &&
203
+ path.node.name.loc
204
+ ) {
205
+ addedInsight = true;
206
+ controller.enqueue({
207
+ entry,
208
+ source: getSourceCodeAt(path.node.name.loc),
209
+ location: convertLocationIntoObject(path.node.name.loc),
210
+ statsPerEmailClient: compatibilityStats.perEmailClient,
211
+ status: compatibilityStats.status,
212
+ });
213
+ }
214
+ }
215
+ },
216
+ });
217
+ }
218
+ }
219
+
220
+ if (entry.category === 'css') {
221
+ const entryFullProperty = getCssPropertyWithValue(entry.title);
222
+ const entryProperties = getCssPropertyNames(
223
+ entry.title,
224
+ entry.keywords,
225
+ );
226
+ const entryUnit = getCssUnit(entry.title);
227
+ const entryFunctions = getCssFunctions(entry.title);
228
+
229
+ const cssEntryType = (() => {
230
+ if (entryFullProperty?.name && entryFullProperty.value) {
231
+ return 'full property';
232
+ }
233
+
234
+ if (entryFunctions.length > 0) {
235
+ return 'function';
236
+ }
237
+
238
+ if (entryUnit) {
239
+ return 'unit';
240
+ }
241
+
242
+ if (entryProperties.length > 0) {
243
+ return 'property name';
244
+ }
245
+ })();
246
+
247
+ if (!cssEntryType) continue;
248
+ const addToInsights = (
249
+ property: StylePropertyUsage & { location: SourceLocation },
250
+ ) => {
251
+ controller.enqueue({
252
+ entry,
253
+ location: convertLocationIntoObject(property.location),
254
+ source: getSourceCodeAt(property.location),
255
+ statsPerEmailClient: compatibilityStats.perEmailClient,
256
+ status: compatibilityStats.status,
257
+ });
258
+ };
259
+
260
+ for (const property of usedStyleProperties) {
261
+ if (!doesPropertyHaveLocation(property)) {
262
+ throw new Error(
263
+ "One of the properties' node did not contain the proper location for it on the source code. This must be an issue because we always need access to the source.",
264
+ {
265
+ cause: {
266
+ property,
267
+ entry,
268
+ reactCode,
269
+ ast,
270
+ },
271
+ },
272
+ );
273
+ }
274
+
275
+ if (cssEntryType === 'full property') {
276
+ if (
277
+ property.name === entryFullProperty?.name &&
278
+ property.value === entryFullProperty.value
279
+ ) {
280
+ addToInsights(property);
281
+ break;
282
+ }
283
+ } else if (cssEntryType === 'function') {
284
+ const functionRegex =
285
+ /(?<functionName>[a-zA-Z_][a-zA-Z0-9_-]*)\s*\(/g;
286
+ const functionName = functionRegex.exec(property.value)?.groups
287
+ ?.functionName;
288
+ if (functionName !== undefined) {
289
+ if (entryFunctions.includes(functionName)) {
290
+ addToInsights(property);
291
+ break;
292
+ }
293
+ }
294
+ } else if (cssEntryType === 'unit') {
295
+ const match = property.value.match(/[0-9](?<unit>[a-zA-Z%]+)$/g);
296
+ if (match) {
297
+ const unit = match.groups?.unit;
298
+ if (entryUnit && unit && entryUnit === unit) {
299
+ addToInsights(property);
300
+ break;
301
+ }
302
+ }
303
+ } else if (
304
+ entryProperties.some(
305
+ (propertyName) => property.name === propertyName,
306
+ )
307
+ ) {
308
+ addToInsights(property);
309
+ break;
310
+ }
311
+ }
312
+ }
313
+ }
314
+ controller.close();
315
+ },
316
+ });
317
+
318
+ return readableStream;
319
+ };
320
+
321
+ export type AST = ReturnType<typeof parse>;
@@ -1,18 +1,12 @@
1
- import { render } from '@react-email/render';
2
1
  import { type ImageCheckingResult, checkImages } from './check-images';
3
2
 
4
3
  test('checkImages()', async () => {
5
4
  const results: ImageCheckingResult[] = [];
6
- const stream = await checkImages(
7
- await render(
8
- <div>
9
- {/* biome-ignore lint/a11y/useAltText: This is intentional to test the checking for accessibility */}
10
- <img src="https://resend.com/static/brand/resend-icon-white.png" />,
11
- <img src="/static/codepen-challengers.png" alt="codepen challenges" />,
12
- </div>,
13
- ),
14
- 'https://demo.react.email',
15
- );
5
+ const html = `<div>
6
+ <img src="https://resend.com/static/brand/resend-icon-white.png" />,
7
+ <img src="/static/codepen-challengers.png" alt="codepen challenges" />,
8
+ </div>`;
9
+ const stream = await checkImages(html, 'https://demo.react.email');
16
10
  const reader = stream.getReader();
17
11
  while (true) {
18
12
  const { done, value } = await reader.read();
@@ -25,7 +19,11 @@ test('checkImages()', async () => {
25
19
  }
26
20
  expect(results).toEqual([
27
21
  {
28
- intendedFor: 'https://resend.com/static/brand/resend-icon-white.png',
22
+ source: 'https://resend.com/static/brand/resend-icon-white.png',
23
+ codeLocation: {
24
+ line: 2,
25
+ column: 3,
26
+ },
29
27
  checks: [
30
28
  {
31
29
  passed: false,
@@ -60,6 +58,10 @@ test('checkImages()', async () => {
60
58
  status: 'warning',
61
59
  },
62
60
  {
61
+ codeLocation: {
62
+ line: 3,
63
+ column: 3,
64
+ },
63
65
  checks: [
64
66
  {
65
67
  metadata: {
@@ -91,7 +93,7 @@ test('checkImages()', async () => {
91
93
  type: 'image_size',
92
94
  },
93
95
  ],
94
- intendedFor: '/static/codepen-challengers.png',
96
+ source: '/static/codepen-challengers.png',
95
97
  status: 'success',
96
98
  },
97
99
  ] satisfies ImageCheckingResult[]);
@@ -2,6 +2,10 @@
2
2
 
3
3
  import type { IncomingMessage } from 'node:http';
4
4
  import { parse } from 'node-html-parser';
5
+ import {
6
+ type CodeLocation,
7
+ getCodeLocationFromAstElement,
8
+ } from './get-code-location-from-ast-element';
5
9
  import { quickFetch } from './quick-fetch';
6
10
 
7
11
  export type ImageCheck = { passed: boolean } & (
@@ -33,7 +37,8 @@ export type ImageCheck = { passed: boolean } & (
33
37
 
34
38
  export interface ImageCheckingResult {
35
39
  status: 'success' | 'warning' | 'error';
36
- intendedFor: string;
40
+ source: string;
41
+ codeLocation: CodeLocation;
37
42
  checks: ImageCheck[];
38
43
  }
39
44
 
@@ -60,7 +65,8 @@ export const checkImages = async (code: string, base: string) => {
60
65
  : rawSource;
61
66
 
62
67
  const result: ImageCheckingResult = {
63
- intendedFor: rawSource,
68
+ source: rawSource,
69
+ codeLocation: getCodeLocationFromAstElement(image, code),
64
70
  status: 'success',
65
71
  checks: [],
66
72
  };
@@ -1,18 +1,14 @@
1
- import { render } from '@react-email/render';
2
1
  import { type LinkCheckingResult, checkLinks } from './check-links';
3
2
 
4
3
  test('checkLinks()', async () => {
5
4
  const results: LinkCheckingResult[] = [];
6
- const stream = await checkLinks(
7
- await render(
8
- <div>
9
- <a href="/">Root</a>
10
- <a href="https://resend.com">Resend</a>
11
- <a href="https://notion.so">Notion</a>
12
- <a href="http://react.email">React Email unsafe</a>
13
- </div>,
14
- ),
15
- );
5
+ const html = `<div>
6
+ <a href="/">Root</a>
7
+ <a href="https://resend.com">Resend</a>
8
+ <a href="https://notion.so">Notion</a>
9
+ <a href="http://react.email">React Email unsafe</a>
10
+ </div>`;
11
+ const stream = await checkLinks(html);
16
12
  const reader = stream.getReader();
17
13
  while (true) {
18
14
  const { done, value } = await reader.read();
@@ -26,16 +22,24 @@ test('checkLinks()', async () => {
26
22
  expect(results).toEqual([
27
23
  {
28
24
  status: 'error',
25
+ codeLocation: {
26
+ line: 2,
27
+ column: 3,
28
+ },
29
29
  checks: [
30
30
  {
31
31
  type: 'syntax',
32
32
  passed: false,
33
33
  },
34
34
  ],
35
- intendedFor: '/',
35
+ link: '/',
36
36
  },
37
37
  {
38
38
  status: 'success',
39
+ codeLocation: {
40
+ line: 3,
41
+ column: 3,
42
+ },
39
43
  checks: [
40
44
  {
41
45
  type: 'syntax',
@@ -53,10 +57,14 @@ test('checkLinks()', async () => {
53
57
  },
54
58
  },
55
59
  ],
56
- intendedFor: 'https://resend.com',
60
+ link: 'https://resend.com',
57
61
  },
58
62
  {
59
63
  status: 'warning',
64
+ codeLocation: {
65
+ line: 4,
66
+ column: 3,
67
+ },
60
68
  checks: [
61
69
  {
62
70
  type: 'syntax',
@@ -74,10 +82,14 @@ test('checkLinks()', async () => {
74
82
  passed: false,
75
83
  },
76
84
  ],
77
- intendedFor: 'https://notion.so',
85
+ link: 'https://notion.so',
78
86
  },
79
87
  {
80
88
  status: 'warning',
89
+ codeLocation: {
90
+ line: 5,
91
+ column: 3,
92
+ },
81
93
  checks: [
82
94
  {
83
95
  type: 'syntax',
@@ -95,7 +107,7 @@ test('checkLinks()', async () => {
95
107
  passed: false,
96
108
  },
97
109
  ],
98
- intendedFor: 'http://react.email',
110
+ link: 'http://react.email',
99
111
  },
100
112
  ] satisfies LinkCheckingResult[]);
101
113
  });
@@ -1,6 +1,10 @@
1
1
  'use server';
2
2
 
3
3
  import { parse } from 'node-html-parser';
4
+ import {
5
+ type CodeLocation,
6
+ getCodeLocationFromAstElement,
7
+ } from './get-code-location-from-ast-element';
4
8
  import { quickFetch } from './quick-fetch';
5
9
 
6
10
  export type LinkCheck = { passed: boolean } & (
@@ -20,7 +24,8 @@ export type LinkCheck = { passed: boolean } & (
20
24
 
21
25
  export interface LinkCheckingResult {
22
26
  status: 'success' | 'warning' | 'error';
23
- intendedFor: string;
27
+ link: string;
28
+ codeLocation: CodeLocation;
24
29
  checks: LinkCheck[];
25
30
  }
26
31
 
@@ -36,7 +41,8 @@ export const checkLinks = async (code: string) => {
36
41
  if (link.startsWith('mailto:')) continue;
37
42
 
38
43
  const result: LinkCheckingResult = {
39
- intendedFor: link,
44
+ link,
45
+ codeLocation: getCodeLocationFromAstElement(anchor, code),
40
46
  status: 'success',
41
47
  checks: [],
42
48
  };
@@ -0,0 +1,18 @@
1
+ import type { HTMLElement } from 'node-html-parser';
2
+ import { getLineAndColumnFromOffset } from '../../utils/get-line-and-column-from-offset';
3
+
4
+ export interface CodeLocation {
5
+ line: number;
6
+ column: number;
7
+ }
8
+
9
+ export const getCodeLocationFromAstElement = (
10
+ ast: HTMLElement,
11
+ html: string,
12
+ ): CodeLocation => {
13
+ const [line, column] = getLineAndColumnFromOffset(ast.range[0], html);
14
+ return {
15
+ line,
16
+ column,
17
+ };
18
+ };
@@ -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, isPreviewDevelopment } 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 && !isPreviewDevelopment) {
39
40
  spinner = ora({
40
41
  text: `Rendering email template ${emailFilename}\n`,
41
42
  prefixText: ' ',
@@ -8,3 +8,8 @@ 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';
13
+
14
+ export const isPreviewDevelopment =
15
+ process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true';
@@ -1,8 +1,8 @@
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';
5
+ import { emailsDirectoryAbsolutePath } from './env';
6
6
  import { inter, sfMono } from './fonts';
7
7
 
8
8
  export const metadata: Metadata = {
package/src/app/page.tsx CHANGED
@@ -4,7 +4,7 @@ import Link from 'next/link';
4
4
  import { Button, Heading, Text } from '../components';
5
5
  import CodeSnippet from '../components/code-snippet';
6
6
  import { Shell, ShellContent } from '../components/shell';
7
- import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
7
+ import { emailsDirectoryAbsolutePath } from './env';
8
8
  import logo from './logo.png';
9
9
 
10
10
  const Home = () => {