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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/index.js +1175 -2659
  3. package/dist/cli/index.mjs +16 -14
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +31 -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 +47 -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/chunks/171.js +14 -0
  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/833.js +1 -0
  38. package/dist/preview/.next/server/functions-config-manifest.json +4 -1
  39. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  40. package/dist/preview/.next/server/pages/500.html +1 -1
  41. package/dist/preview/.next/server/pages/_app.js +1 -1
  42. package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
  43. package/dist/preview/.next/server/pages/_document.js +1 -1
  44. package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
  45. package/dist/preview/.next/server/pages/_error.js +1 -1
  46. package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
  47. package/dist/preview/.next/server/pages-manifest.json +5 -1
  48. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  49. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  51. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
  52. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
  53. package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
  54. package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
  55. package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
  56. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
  57. package/dist/preview/.next/static/chunks/{afa401a5-a600c227dacf3ab4.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
  58. package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
  59. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
  60. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
  61. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
  62. package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
  63. package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
  64. package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
  65. package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
  66. package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
  67. package/dist/preview/.next/static/chunks/{webpack-2eb145a20ee6cb77.js → webpack-41e2667c9f086a4f.js} +1 -1
  68. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
  69. package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
  70. package/dist/preview/.next/trace +26 -22
  71. package/dist/preview/.next/types/cache-life.d.ts +3 -3
  72. package/package.json +14 -9
  73. package/scripts/build-preview-server.mjs +32 -0
  74. package/scripts/fill-caniemail-data.mjs +36 -0
  75. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  76. package/src/actions/email-validation/check-compatibility.ts +322 -0
  77. package/src/actions/email-validation/check-images.spec.tsx +2 -2
  78. package/src/actions/email-validation/check-images.ts +2 -2
  79. package/src/actions/email-validation/check-links.spec.tsx +4 -4
  80. package/src/actions/email-validation/check-links.ts +2 -2
  81. package/src/actions/get-email-path-from-slug.ts +1 -1
  82. package/src/actions/render-email-by-path.tsx +2 -1
  83. package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
  84. package/src/app/layout.tsx +1 -1
  85. package/src/app/page.tsx +1 -1
  86. package/src/app/preview/[...slug]/page.tsx +73 -16
  87. package/src/app/preview/[...slug]/preview.tsx +11 -57
  88. package/src/components/code.tsx +0 -1
  89. package/src/components/toolbar/linter.tsx +267 -124
  90. package/src/components/toolbar/spam-assassin.tsx +20 -31
  91. package/src/components/toolbar/toolbar-button.tsx +50 -0
  92. package/src/components/toolbar/use-cached-state.ts +33 -0
  93. package/src/components/toolbar.tsx +106 -98
  94. package/src/components/topbar/view-size-controls.tsx +1 -0
  95. package/src/components/topbar.tsx +3 -9
  96. package/src/contexts/emails.tsx +2 -1
  97. package/src/contexts/preview.tsx +81 -0
  98. package/src/hooks/use-email-rendering-result.ts +2 -1
  99. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  100. package/src/utils/caniemail/all-css-properties.ts +358 -0
  101. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  102. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  103. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  104. package/src/utils/caniemail/get-css-functions.ts +25 -0
  105. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  106. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  107. package/src/utils/caniemail/get-css-unit.ts +3 -0
  108. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  109. package/src/utils/caniemail/get-element-names.ts +20 -0
  110. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  111. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
  112. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  113. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  114. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  115. package/src/utils/get-email-component.ts +34 -67
  116. package/src/utils/linting.ts +85 -0
  117. package/src/utils/result.ts +49 -0
  118. package/src/utils/run-bundled-code.ts +64 -0
  119. package/tailwind-internals.d.ts +133 -0
  120. package/tsconfig.json +9 -3
  121. package/build-preview-server.mjs +0 -25
  122. package/dist/preview/.next/cache/images/TcyzHbFXGFjrOu3wEMvDoSmqCh3qP3iiNqJf0QbED9Y/60.1741728556140.cQ5qicbpvoXZ7leVmWqG2ElLwXB1ynYeSv8MBSA-QeM.Vy8iMWM3MGUtMTk1ODcxYmIyNzMi.webp +0 -0
  123. package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
  124. package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
  125. package/dist/preview/.next/cache/webpack/client-development/10.pack.gz +0 -0
  126. package/dist/preview/.next/cache/webpack/client-development/11.pack.gz +0 -0
  127. package/dist/preview/.next/cache/webpack/client-development/12.pack.gz +0 -0
  128. package/dist/preview/.next/cache/webpack/client-development/13.pack.gz +0 -0
  129. package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
  130. package/dist/preview/.next/cache/webpack/client-development/3.pack.gz +0 -0
  131. package/dist/preview/.next/cache/webpack/client-development/4.pack.gz +0 -0
  132. package/dist/preview/.next/cache/webpack/client-development/5.pack.gz +0 -0
  133. package/dist/preview/.next/cache/webpack/client-development/6.pack.gz +0 -0
  134. package/dist/preview/.next/cache/webpack/client-development/7.pack.gz +0 -0
  135. package/dist/preview/.next/cache/webpack/client-development/8.pack.gz +0 -0
  136. package/dist/preview/.next/cache/webpack/client-development/9.pack.gz +0 -0
  137. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
  138. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  139. package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
  140. package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
  141. package/dist/preview/.next/cache/webpack/server-development/2.pack.gz +0 -0
  142. package/dist/preview/.next/cache/webpack/server-development/3.pack.gz +0 -0
  143. package/dist/preview/.next/cache/webpack/server-development/4.pack.gz +0 -0
  144. package/dist/preview/.next/cache/webpack/server-development/5.pack.gz +0 -0
  145. package/dist/preview/.next/cache/webpack/server-development/6.pack.gz +0 -0
  146. package/dist/preview/.next/cache/webpack/server-development/7.pack.gz +0 -0
  147. package/dist/preview/.next/cache/webpack/server-development/8.pack.gz +0 -0
  148. package/dist/preview/.next/cache/webpack/server-development/9.pack.gz +0 -0
  149. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
  150. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  151. package/dist/preview/.next/server/chunks/143.js +0 -6
  152. package/dist/preview/.next/server/chunks/409.js +0 -5
  153. package/dist/preview/.next/server/chunks/46.js +0 -1
  154. package/dist/preview/.next/server/chunks/478.js +0 -14
  155. package/dist/preview/.next/server/chunks/707.js +0 -13
  156. package/dist/preview/.next/static/B4EYZiVzdylEG9lAIl-aO/_buildManifest.js +0 -1
  157. package/dist/preview/.next/static/chunks/575-bc52750855c25df4.js +0 -2
  158. package/dist/preview/.next/static/chunks/684-0f1ef7361c499798.js +0 -1
  159. package/dist/preview/.next/static/chunks/684c6b30-0c65da32762fc4ee.js +0 -1
  160. package/dist/preview/.next/static/chunks/81-e7539b08d9d3fb4d.js +0 -1
  161. package/dist/preview/.next/static/chunks/883-70c8267c50bc4133.js +0 -1
  162. package/dist/preview/.next/static/chunks/921-d1dc8c63f49e85d6.js +0 -1
  163. package/dist/preview/.next/static/chunks/app/_not-found/page-03ce767859c36d4e.js +0 -1
  164. package/dist/preview/.next/static/chunks/app/layout-7cf14e28880544f1.js +0 -1
  165. package/dist/preview/.next/static/chunks/app/page-065cb49b0a078541.js +0 -1
  166. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-656510fd180c803c.js +0 -1
  167. package/dist/preview/.next/static/chunks/framework-2a724981073c3a29.js +0 -1
  168. package/dist/preview/.next/static/chunks/main-552b9719bbc3a274.js +0 -1
  169. package/dist/preview/.next/static/chunks/main-app-914a73336fd45af5.js +0 -1
  170. package/dist/preview/.next/static/chunks/pages/_app-77ca34bce25ac75c.js +0 -1
  171. package/dist/preview/.next/static/chunks/pages/_error-73f611c46abbb495.js +0 -1
  172. package/dist/preview/.next/static/css/2df96d9ee014e8de.css +0 -3
  173. /package/dist/preview/.next/static/{B4EYZiVzdylEG9lAIl-aO → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
@@ -0,0 +1,322 @@
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
+ 'hey',
116
+ 'outlook',
117
+ 'yahoo',
118
+ ];
119
+
120
+ export const checkCompatibility = async (
121
+ reactCode: string,
122
+ emailPath: string,
123
+ ) => {
124
+ const ast = parse(reactCode, {
125
+ strictMode: false,
126
+ errorRecovery: true,
127
+ sourceType: 'unambiguous',
128
+ plugins: ['jsx', 'typescript', 'decorators'],
129
+ });
130
+
131
+ const getSourceCodeAt = (location: SourceLocation) => {
132
+ const codeLines = reactCode.split(/\n|\r|\r\n/);
133
+ const source = codeLines
134
+ .slice(
135
+ Math.max(location.start.line - 2, 0),
136
+ Math.min(location.end.line + 2, codeLines.length),
137
+ )
138
+ .join('\n');
139
+ return source;
140
+ };
141
+
142
+ const objectVariables = getObjectVariables(ast);
143
+ const usedStyleProperties = await getUsedStyleProperties(
144
+ ast,
145
+ reactCode,
146
+ emailPath,
147
+ objectVariables,
148
+ );
149
+ const readableStream = new ReadableStream<CompatibilityCheckingResult>({
150
+ async start(controller) {
151
+ for (const entry of supportEntries) {
152
+ const compatibilityStats = getCompatibilityStatsForEntry(
153
+ entry,
154
+ relevantEmailClients,
155
+ );
156
+ if (Object.keys(compatibilityStats.perEmailClient).length === 0)
157
+ continue;
158
+ if (compatibilityStats.status === 'success') continue;
159
+
160
+ if (entry.category === 'html') {
161
+ const entryElements = getElementNames(entry.title, entry.keywords);
162
+ const entryAttributes = getElementAttributes(entry.title);
163
+ const htmlEntryType = (() => {
164
+ if (entryElements.length > 0) {
165
+ return 'element';
166
+ }
167
+
168
+ if (entryAttributes.length > 0) {
169
+ return 'attribute';
170
+ }
171
+ })();
172
+
173
+ if (!htmlEntryType) continue;
174
+
175
+ let addedInsight = false;
176
+ if (htmlEntryType === 'element') {
177
+ traverse(ast, {
178
+ JSXOpeningElement(path) {
179
+ if (path.node.name.type === 'JSXIdentifier' && !addedInsight) {
180
+ const elementName = path.node.name.name;
181
+ if (
182
+ entryElements.includes(elementName) &&
183
+ path.node.name.loc
184
+ ) {
185
+ addedInsight = true;
186
+ controller.enqueue({
187
+ entry,
188
+ source: getSourceCodeAt(path.node.name.loc),
189
+ location: convertLocationIntoObject(path.node.name.loc),
190
+ statsPerEmailClient: compatibilityStats.perEmailClient,
191
+ status: compatibilityStats.status,
192
+ });
193
+ }
194
+ }
195
+ },
196
+ });
197
+ } else {
198
+ traverse(ast, {
199
+ JSXAttribute(path) {
200
+ if (path.node.name.type === 'JSXIdentifier' && !addedInsight) {
201
+ const attributeName = path.node.name.name;
202
+ if (
203
+ entryAttributes.includes(attributeName) &&
204
+ path.node.name.loc
205
+ ) {
206
+ addedInsight = true;
207
+ controller.enqueue({
208
+ entry,
209
+ source: getSourceCodeAt(path.node.name.loc),
210
+ location: convertLocationIntoObject(path.node.name.loc),
211
+ statsPerEmailClient: compatibilityStats.perEmailClient,
212
+ status: compatibilityStats.status,
213
+ });
214
+ }
215
+ }
216
+ },
217
+ });
218
+ }
219
+ }
220
+
221
+ if (entry.category === 'css') {
222
+ const entryFullProperty = getCssPropertyWithValue(entry.title);
223
+ const entryProperties = getCssPropertyNames(
224
+ entry.title,
225
+ entry.keywords,
226
+ );
227
+ const entryUnit = getCssUnit(entry.title);
228
+ const entryFunctions = getCssFunctions(entry.title);
229
+
230
+ const cssEntryType = (() => {
231
+ if (entryFullProperty?.name && entryFullProperty.value) {
232
+ return 'full property';
233
+ }
234
+
235
+ if (entryFunctions.length > 0) {
236
+ return 'function';
237
+ }
238
+
239
+ if (entryUnit) {
240
+ return 'unit';
241
+ }
242
+
243
+ if (entryProperties.length > 0) {
244
+ return 'property name';
245
+ }
246
+ })();
247
+
248
+ if (!cssEntryType) continue;
249
+ const addToInsights = (
250
+ property: StylePropertyUsage & { location: SourceLocation },
251
+ ) => {
252
+ controller.enqueue({
253
+ entry,
254
+ location: convertLocationIntoObject(property.location),
255
+ source: getSourceCodeAt(property.location),
256
+ statsPerEmailClient: compatibilityStats.perEmailClient,
257
+ status: compatibilityStats.status,
258
+ });
259
+ };
260
+
261
+ for (const property of usedStyleProperties) {
262
+ if (!doesPropertyHaveLocation(property)) {
263
+ throw new Error(
264
+ "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.",
265
+ {
266
+ cause: {
267
+ property,
268
+ entry,
269
+ reactCode,
270
+ ast,
271
+ },
272
+ },
273
+ );
274
+ }
275
+
276
+ if (cssEntryType === 'full property') {
277
+ if (
278
+ property.name === entryFullProperty?.name &&
279
+ property.value === entryFullProperty.value
280
+ ) {
281
+ addToInsights(property);
282
+ break;
283
+ }
284
+ } else if (cssEntryType === 'function') {
285
+ const functionRegex =
286
+ /(?<functionName>[a-zA-Z_][a-zA-Z0-9_-]*)\s*\(/g;
287
+ const functionName = functionRegex.exec(property.value)?.groups
288
+ ?.functionName;
289
+ if (functionName !== undefined) {
290
+ if (entryFunctions.includes(functionName)) {
291
+ addToInsights(property);
292
+ break;
293
+ }
294
+ }
295
+ } else if (cssEntryType === 'unit') {
296
+ const match = property.value.match(/[0-9](?<unit>[a-zA-Z%]+)$/g);
297
+ if (match) {
298
+ const unit = match.groups?.unit;
299
+ if (entryUnit && unit && entryUnit === unit) {
300
+ addToInsights(property);
301
+ break;
302
+ }
303
+ }
304
+ } else if (
305
+ entryProperties.some(
306
+ (propertyName) => property.name === propertyName,
307
+ )
308
+ ) {
309
+ addToInsights(property);
310
+ break;
311
+ }
312
+ }
313
+ }
314
+ }
315
+ controller.close();
316
+ },
317
+ });
318
+
319
+ return readableStream;
320
+ };
321
+
322
+ export type AST = ReturnType<typeof parse>;
@@ -25,7 +25,7 @@ test('checkImages()', async () => {
25
25
  }
26
26
  expect(results).toEqual([
27
27
  {
28
- intendedFor: 'https://resend.com/static/brand/resend-icon-white.png',
28
+ source: 'https://resend.com/static/brand/resend-icon-white.png',
29
29
  checks: [
30
30
  {
31
31
  passed: false,
@@ -91,7 +91,7 @@ test('checkImages()', async () => {
91
91
  type: 'image_size',
92
92
  },
93
93
  ],
94
- intendedFor: '/static/codepen-challengers.png',
94
+ source: '/static/codepen-challengers.png',
95
95
  status: 'success',
96
96
  },
97
97
  ] satisfies ImageCheckingResult[]);
@@ -33,7 +33,7 @@ export type ImageCheck = { passed: boolean } & (
33
33
 
34
34
  export interface ImageCheckingResult {
35
35
  status: 'success' | 'warning' | 'error';
36
- intendedFor: string;
36
+ source: string;
37
37
  checks: ImageCheck[];
38
38
  }
39
39
 
@@ -60,7 +60,7 @@ export const checkImages = async (code: string, base: string) => {
60
60
  : rawSource;
61
61
 
62
62
  const result: ImageCheckingResult = {
63
- intendedFor: rawSource,
63
+ source: rawSource,
64
64
  status: 'success',
65
65
  checks: [],
66
66
  };
@@ -32,7 +32,7 @@ test('checkLinks()', async () => {
32
32
  passed: false,
33
33
  },
34
34
  ],
35
- intendedFor: '/',
35
+ link: '/',
36
36
  },
37
37
  {
38
38
  status: 'success',
@@ -53,7 +53,7 @@ test('checkLinks()', async () => {
53
53
  },
54
54
  },
55
55
  ],
56
- intendedFor: 'https://resend.com',
56
+ link: 'https://resend.com',
57
57
  },
58
58
  {
59
59
  status: 'warning',
@@ -74,7 +74,7 @@ test('checkLinks()', async () => {
74
74
  passed: false,
75
75
  },
76
76
  ],
77
- intendedFor: 'https://notion.so',
77
+ link: 'https://notion.so',
78
78
  },
79
79
  {
80
80
  status: 'warning',
@@ -95,7 +95,7 @@ test('checkLinks()', async () => {
95
95
  passed: false,
96
96
  },
97
97
  ],
98
- intendedFor: 'http://react.email',
98
+ link: 'http://react.email',
99
99
  },
100
100
  ] satisfies LinkCheckingResult[]);
101
101
  });
@@ -20,7 +20,7 @@ export type LinkCheck = { passed: boolean } & (
20
20
 
21
21
  export interface LinkCheckingResult {
22
22
  status: 'success' | 'warning' | 'error';
23
- intendedFor: string;
23
+ link: string;
24
24
  checks: LinkCheck[];
25
25
  }
26
26
 
@@ -36,7 +36,7 @@ export const checkLinks = async (code: string) => {
36
36
  if (link.startsWith('mailto:')) continue;
37
37
 
38
38
  const result: LinkCheckingResult = {
39
- intendedFor: link,
39
+ link,
40
40
  status: 'success',
41
41
  checks: [],
42
42
  };
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { cache } from 'react';
5
- import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
5
+ import { emailsDirectoryAbsolutePath } from '../app/env';
6
6
 
7
7
  // eslint-disable-next-line @typescript-eslint/require-await
8
8
  export const getEmailPathFromSlug = cache(async (slug: string) => {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import logSymbols from 'log-symbols';
6
6
  import ora from 'ora';
7
+ import { isBuilding } from '../app/env';
7
8
  import { getEmailComponent } from '../utils/get-email-component';
8
9
  import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
9
10
  import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';
@@ -35,7 +36,7 @@ export const renderEmailByPath = async (
35
36
 
36
37
  const emailFilename = path.basename(emailPath);
37
38
  let spinner: ora.Ora | undefined;
38
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
39
+ if (!isBuilding) {
39
40
  spinner = ora({
40
41
  text: `Rendering email template ${emailFilename}\n`,
41
42
  prefixText: ' ',
@@ -8,3 +8,5 @@ export const userProjectLocation = process.env.USER_PROJECT_LOCATION!;
8
8
  /** ONLY ACCESSIBLE ON THE SERVER */
9
9
  export const emailsDirectoryAbsolutePath =
10
10
  process.env.EMAILS_DIR_ABSOLUTE_PATH!;
11
+
12
+ export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
@@ -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 = () => {
@@ -3,8 +3,14 @@ import { redirect } from 'next/navigation';
3
3
  import { Suspense } from 'react';
4
4
  import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
5
5
  import { renderEmailByPath } from '../../../actions/render-email-by-path';
6
- import { emailsDirectoryAbsolutePath } from '../../../utils/emails-directory-absolute-path';
6
+ import { Shell } from '../../../components/shell';
7
+ import { Toolbar } from '../../../components/toolbar';
8
+ import type { LintingRow } from '../../../components/toolbar/linter';
9
+ import type { SpamCheckingResult } from '../../../components/toolbar/spam-assassin';
10
+ import { PreviewProvider } from '../../../contexts/preview';
7
11
  import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
12
+ import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
13
+ import { emailsDirectoryAbsolutePath, isBuilding } from '../../env';
8
14
  import Home from '../../page';
9
15
  import Preview from './preview';
10
16
 
@@ -50,27 +56,78 @@ This is most likely not an issue with the preview server. Maybe there was a typo
50
56
 
51
57
  const serverEmailRenderingResult = await renderEmailByPath(emailPath);
52
58
 
53
- if (
54
- process.env.NEXT_PUBLIC_IS_BUILDING === 'true' &&
55
- 'error' in serverEmailRenderingResult
56
- ) {
59
+ if (isBuilding && 'error' in serverEmailRenderingResult) {
57
60
  throw new Error(serverEmailRenderingResult.error.message, {
58
61
  cause: serverEmailRenderingResult.error,
59
62
  });
60
63
  }
61
64
 
65
+ let spamCheckingResult: SpamCheckingResult | undefined = undefined;
66
+ let lintingRows: LintingRow[] | undefined = undefined;
67
+
68
+ if (isBuilding && !('error' in serverEmailRenderingResult)) {
69
+ const lintingSources = getLintingSources(
70
+ serverEmailRenderingResult.markup,
71
+ serverEmailRenderingResult.reactMarkup,
72
+ emailPath,
73
+ '',
74
+ );
75
+ lintingRows = [];
76
+ for await (const row of loadLintingRowsFrom(lintingSources)) {
77
+ lintingRows.push(row);
78
+ }
79
+ lintingRows.sort((a, b) => {
80
+ if (a.result.status === 'error' && b.result.status === 'warning') {
81
+ return -1;
82
+ }
83
+
84
+ if (a.result.status === 'warning' && b.result.status === 'error') {
85
+ return 1;
86
+ }
87
+
88
+ return 0;
89
+ });
90
+
91
+ const response = await fetch('https://react.email/api/check-spam', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({
95
+ html: serverEmailRenderingResult.markup,
96
+ plainText: serverEmailRenderingResult.plainText,
97
+ }),
98
+ });
99
+ const responseBody = (await response.json()) as
100
+ | { error: string }
101
+ | SpamCheckingResult;
102
+ if ('error' in responseBody) {
103
+ throw new Error(`Failed doing Spam Check. ${responseBody.error}`, {
104
+ cause: responseBody,
105
+ });
106
+ }
107
+
108
+ spamCheckingResult = responseBody;
109
+ }
110
+
62
111
  return (
63
- // This suspense is so that this page doesn't throw warnings
64
- // on the build of the preview server de-opting into
65
- // client-side rendering on build
66
- <Suspense fallback={<Home />}>
67
- <Preview
68
- emailPath={emailPath}
69
- pathSeparator={path.sep}
70
- serverRenderingResult={serverEmailRenderingResult}
71
- slug={slug}
72
- />
73
- </Suspense>
112
+ <PreviewProvider
113
+ emailSlug={slug}
114
+ emailPath={emailPath}
115
+ serverRenderingResult={serverEmailRenderingResult}
116
+ >
117
+ <Shell currentEmailOpenSlug={slug}>
118
+ {/* This suspense is so that this page doesn't throw warnings */}
119
+ {/* on the build of the preview server de-opting into */}
120
+ {/* client-side rendering on build */}
121
+ <Suspense fallback={<Home />}>
122
+ <Preview emailTitle={path.basename(emailPath)} />
123
+
124
+ <Toolbar
125
+ serverLintingRows={lintingRows}
126
+ serverSpamCheckingResult={spamCheckingResult}
127
+ />
128
+ </Suspense>
129
+ </Shell>
130
+ </PreviewProvider>
74
131
  );
75
132
  };
76
133