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,7 @@
1
+ export function getElementAttributes(title: string) {
2
+ if (title.endsWith(' attribute')) {
3
+ return [title.replace(' attribute', '')];
4
+ }
5
+
6
+ return [];
7
+ }
@@ -0,0 +1,20 @@
1
+ export const getElementNames = (title: string, keywords: string | null) => {
2
+ const match = /<(?<elementName>[^>]*)> element/.exec(title);
3
+ if (match) {
4
+ const [_full, elementName] = match;
5
+
6
+ if (elementName) {
7
+ return [elementName];
8
+ }
9
+ }
10
+
11
+ if (keywords !== null && keywords.length > 0) {
12
+ return keywords.split(/\s*,\s*/).map((piece) => piece.trim());
13
+ }
14
+
15
+ if (title.split(',').length > 1) {
16
+ return title.split(/\s*,\s*/).map((piece) => piece.trim());
17
+ }
18
+
19
+ return [];
20
+ };
@@ -0,0 +1,30 @@
1
+ import type { Root, Rule } from 'postcss';
2
+ import postcss from 'postcss';
3
+ import evaluateTailwindFunctions from 'tailwindcss/lib/lib/evaluateTailwindFunctions';
4
+ import { generateRules as rawGenerateRules } from 'tailwindcss/lib/lib/generateRules';
5
+ import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils';
6
+
7
+ export const generateTailwindCssRules = (
8
+ classNames: string[],
9
+ tailwindContext: JitContext,
10
+ ): { root: Root; rules: Rule[] } => {
11
+ const bigIntRuleTuples: [bigint, Rule][] = rawGenerateRules(
12
+ new Set(classNames),
13
+ tailwindContext,
14
+ );
15
+
16
+ const root = postcss.root({
17
+ nodes: bigIntRuleTuples.map(([, rule]) => rule),
18
+ });
19
+ evaluateTailwindFunctions(tailwindContext)(root);
20
+
21
+ const actualRules: Rule[] = [];
22
+ root.walkRules((rule) => {
23
+ actualRules.push(rule);
24
+ });
25
+
26
+ return {
27
+ root,
28
+ rules: actualRules,
29
+ };
30
+ };
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { Node } from '@babel/traverse';
4
+ import traverse from '@babel/traverse';
5
+ import * as esbuild from 'esbuild';
6
+ import type { Config as TailwindOriginalConfig } from 'tailwindcss';
7
+ import type { AST } from '../../../actions/email-validation/check-compatibility';
8
+ import { isErr } from '../../result';
9
+ import { runBundledCode } from '../../run-bundled-code';
10
+
11
+ export type TailwindConfig = Pick<
12
+ TailwindOriginalConfig,
13
+ | 'important'
14
+ | 'prefix'
15
+ | 'separator'
16
+ | 'safelist'
17
+ | 'blocklist'
18
+ | 'presets'
19
+ | 'future'
20
+ | 'experimental'
21
+ | 'darkMode'
22
+ | 'theme'
23
+ | 'corePlugins'
24
+ | 'plugins'
25
+ >;
26
+
27
+ const getFirstExistingFilepath = (filePaths: string[]) => {
28
+ for (const filePath of filePaths) {
29
+ if (fs.existsSync(filePath)) {
30
+ return filePath;
31
+ }
32
+ }
33
+ };
34
+
35
+ type ImportDeclaration = Node & { type: 'ImportDeclaration' };
36
+
37
+ export const getTailwindConfig = async (
38
+ sourceCode: string,
39
+ ast: AST,
40
+ sourcePath: string,
41
+ ): Promise<TailwindConfig> => {
42
+ const configAttribute = getTailwindConfigNode(ast);
43
+
44
+ if (configAttribute) {
45
+ const configIdentifierName =
46
+ configAttribute.value?.type === 'JSXExpressionContainer' &&
47
+ configAttribute.value.expression.type === 'Identifier'
48
+ ? configAttribute.value.expression.name
49
+ : undefined;
50
+ if (configIdentifierName) {
51
+ const tailwindConfigImport = getImportWithGivenDefaultSpecifier(
52
+ ast,
53
+ configIdentifierName,
54
+ );
55
+ if (tailwindConfigImport) {
56
+ return getConfigFromImport(tailwindConfigImport, sourcePath);
57
+ }
58
+ }
59
+
60
+ const configObjectExpression =
61
+ configAttribute.value?.type === 'JSXExpressionContainer' &&
62
+ configAttribute.value.expression.type === 'ObjectExpression'
63
+ ? configAttribute.value.expression
64
+ : undefined;
65
+ if (configObjectExpression?.start && configObjectExpression.end) {
66
+ const configObjectSourceCode = sourceCode.slice(
67
+ configObjectExpression.start,
68
+ configObjectExpression.end,
69
+ );
70
+
71
+ try {
72
+ const getConfig = new Function(`return ${configObjectSourceCode}`);
73
+ return getConfig() as TailwindConfig;
74
+ } catch (exception) {
75
+ console.warn(exception);
76
+ console.warn(
77
+ `Tried reading the config defined directly in the Tailwind component but was unable to, probably because it isn't a valid javascript object by itself.`,
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ return {};
84
+ };
85
+
86
+ const getConfigFromImport = async (
87
+ tailwindConfigImport: ImportDeclaration,
88
+ sourcePath: string,
89
+ ): Promise<TailwindConfig> => {
90
+ const configRelativePath = tailwindConfigImport.source.value;
91
+ const configImportPath = path.resolve(
92
+ path.dirname(sourcePath),
93
+ configRelativePath,
94
+ );
95
+ const configFilepath = getFirstExistingFilepath([
96
+ configImportPath,
97
+ `${configImportPath}.ts`,
98
+ `${configImportPath}.js`,
99
+ `${configImportPath}.mjs`,
100
+ `${configImportPath}.cjs`,
101
+ ]);
102
+
103
+ if (configFilepath === undefined) {
104
+ throw new Error(
105
+ `Could not find Tailwind config by inferring it's extension type (tried .ts, .js, .mjs and .cjs).`,
106
+ {
107
+ cause: {
108
+ configPath: configImportPath,
109
+ sourcePath,
110
+ },
111
+ },
112
+ );
113
+ }
114
+
115
+ const configBuildResult = await esbuild.build({
116
+ bundle: false,
117
+ entryPoints: [configFilepath],
118
+ platform: 'node',
119
+ write: false,
120
+ format: 'cjs',
121
+ logLevel: 'silent',
122
+ });
123
+ const configFile = configBuildResult.outputFiles[0];
124
+ if (configFile === undefined) {
125
+ throw new Error(
126
+ 'Could not build config file as it was found as undefined, this is a bug please open an issue.',
127
+ );
128
+ }
129
+ const configModule = runBundledCode(configFile.text, configFilepath);
130
+ if (isErr(configModule)) {
131
+ throw new Error('Error when trying to run the config file', {
132
+ cause: configModule.error,
133
+ });
134
+ }
135
+
136
+ console.log(configModule);
137
+
138
+ if (
139
+ typeof configModule.value === 'object' &&
140
+ configModule.value !== null &&
141
+ 'default' in configModule.value
142
+ ) {
143
+ return configModule.value.default as TailwindConfig;
144
+ }
145
+
146
+ throw new Error(
147
+ `Could not read Tailwind config at ${configFilepath} because it doesn't have a default export in it.`,
148
+ {
149
+ cause: {
150
+ configModule,
151
+ configFilepath,
152
+ },
153
+ },
154
+ );
155
+ };
156
+
157
+ const getImportWithGivenDefaultSpecifier = (
158
+ ast: AST,
159
+ specifierName: string,
160
+ ) => {
161
+ let importNode: ImportDeclaration | undefined;
162
+ traverse(ast, {
163
+ ImportDeclaration(nodePath) {
164
+ if (
165
+ nodePath.node.specifiers.some(
166
+ (specifier) =>
167
+ specifier.type === 'ImportDefaultSpecifier' &&
168
+ specifier.local.name === specifierName,
169
+ )
170
+ ) {
171
+ importNode = nodePath.node;
172
+ }
173
+ },
174
+ });
175
+ return importNode;
176
+ };
177
+
178
+ type JSXAttribute = Node & { type: 'JSXAttribute' };
179
+
180
+ const getTailwindConfigNode = (ast: AST) => {
181
+ let tailwindConfigNode: JSXAttribute | undefined;
182
+ traverse(ast, {
183
+ JSXOpeningElement(nodePath) {
184
+ if (
185
+ nodePath.node.name.type === 'JSXIdentifier' &&
186
+ nodePath.node.name.name === 'Tailwind'
187
+ ) {
188
+ const configAttribute = nodePath.node.attributes.find(
189
+ (
190
+ attribute,
191
+ ): attribute is Node & {
192
+ type: 'JSXAttribute';
193
+ } =>
194
+ attribute.type === 'JSXAttribute' &&
195
+ attribute.name.type === 'JSXIdentifier' &&
196
+ attribute.name.name === 'config',
197
+ );
198
+ if (configAttribute) {
199
+ tailwindConfigNode = configAttribute;
200
+ }
201
+ }
202
+ },
203
+ });
204
+ return tailwindConfigNode;
205
+ };
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parse } from '@babel/parser';
4
+ import { getTailwindMetadata } from './get-tailwind-metadata';
5
+
6
+ describe('getTailwindMetadata()', () => {
7
+ test('with the netlify-welcome demo email', async () => {
8
+ const emailPath = path.resolve(
9
+ __dirname,
10
+ '../../../../../../apps/demo/emails/welcome/netlify-welcome.tsx',
11
+ );
12
+ const reactCode = await fs.readFile(emailPath, 'utf8');
13
+ const ast = parse(reactCode, {
14
+ strictMode: false,
15
+ errorRecovery: true,
16
+ sourceType: 'unambiguous',
17
+ plugins: ['jsx', 'typescript', 'decorators'],
18
+ });
19
+
20
+ const tailwindMetadata = getTailwindMetadata(ast, reactCode, emailPath);
21
+
22
+ expect(tailwindMetadata).toBeDefined();
23
+ // console.log(tailwindMetadata);
24
+ });
25
+ });
@@ -0,0 +1,45 @@
1
+ import traverse from '@babel/traverse';
2
+ import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils';
3
+ import type { AST } from '../../../actions/email-validation/check-compatibility';
4
+ import { type TailwindConfig, getTailwindConfig } from './get-tailwind-config';
5
+ import { setupTailwindContext } from './setup-tailwind-context';
6
+
7
+ export const getTailwindMetadata = async (
8
+ ast: AST,
9
+ sourceCode: string,
10
+ sourcePath: string,
11
+ ): Promise<
12
+ | {
13
+ hasTailwind: false;
14
+ }
15
+ | {
16
+ hasTailwind: true;
17
+ config: TailwindConfig;
18
+ context: JitContext;
19
+ }
20
+ > => {
21
+ let hasTailwind = false as boolean;
22
+ traverse(ast, {
23
+ JSXOpeningElement(path) {
24
+ if (
25
+ path.node.name.type === 'JSXIdentifier' &&
26
+ path.node.name.name === 'Tailwind'
27
+ ) {
28
+ hasTailwind = true;
29
+ }
30
+ },
31
+ });
32
+
33
+ if (!hasTailwind) {
34
+ return { hasTailwind: false };
35
+ }
36
+
37
+ const config = await getTailwindConfig(sourceCode, ast, sourcePath);
38
+ const context = setupTailwindContext(config);
39
+
40
+ return {
41
+ hasTailwind: true,
42
+ config,
43
+ context,
44
+ };
45
+ };
@@ -0,0 +1,15 @@
1
+ import { createContext } from 'tailwindcss/lib/lib/setupContextUtils';
2
+ import resolveConfig from 'tailwindcss/resolveConfig';
3
+ import type { TailwindConfig } from './get-tailwind-config';
4
+
5
+ export const setupTailwindContext = (config: TailwindConfig) => {
6
+ return createContext(
7
+ resolveConfig({
8
+ ...config,
9
+ content: [],
10
+ corePlugins: {
11
+ preflight: false,
12
+ },
13
+ }),
14
+ );
15
+ };
@@ -1,16 +1,22 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
1
  import path from 'node:path';
3
- import vm from 'node:vm';
4
2
  import type { render } from '@react-email/render';
5
3
  import { type BuildFailure, type OutputFile, build } from 'esbuild';
6
4
  import type React from 'react';
7
5
  import type { RawSourceMap } from 'source-map-js';
6
+ import { z } from 'zod';
8
7
  import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
9
8
  import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
10
- import { staticNodeModulesForVM } from './static-node-modules-for-vm';
9
+ import { isErr } from './result';
10
+ import { runBundledCode } from './run-bundled-code';
11
11
  import type { EmailTemplate as EmailComponent } from './types/email-template';
12
12
  import type { ErrorObject } from './types/error-object';
13
13
 
14
+ const EmailComponentModule = z.object({
15
+ default: z.function(),
16
+ render: z.function(),
17
+ reactEmailCreateReactElement: z.function(),
18
+ });
19
+
14
20
  export const getEmailComponent = async (
15
21
  emailPath: string,
16
22
  ): Promise<
@@ -61,79 +67,38 @@ export const getEmailComponent = async (
61
67
  const bundledEmailFile = outputFiles[1]!;
62
68
  const builtEmailCode = bundledEmailFile.text;
63
69
 
64
- const fakeContext = {
65
- ...global,
66
- console,
67
- Buffer,
68
- AbortSignal,
69
- Event,
70
- EventTarget,
71
- TextDecoder,
72
- Request,
73
- Response,
74
- TextDecoderStream,
75
- TextEncoder,
76
- TextEncoderStream,
77
- ReadableStream,
78
- URL,
79
- URLSearchParams,
80
- Headers,
81
- module: {
82
- exports: {
83
- default: undefined as unknown,
84
- render: undefined as unknown,
85
- reactEmailCreateReactElement: undefined as unknown,
86
- },
87
- },
88
- __filename: emailPath,
89
- __dirname: path.dirname(emailPath),
90
- require: (specifiedModule: string) => {
91
- let m = specifiedModule;
92
- if (specifiedModule.startsWith('node:')) {
93
- m = m.split(':')[1]!;
94
- }
95
-
96
- if (m in staticNodeModulesForVM) {
97
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
98
- return staticNodeModulesForVM[m];
99
- }
100
-
101
- // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
102
- return require(`${specifiedModule}`) as unknown;
103
- // this stupid string templating was necessary to not have
104
- // webpack warnings like:
105
- //
106
- // Import trace for requested module:
107
- // ./src/utils/get-email-component.tsx
108
- // ./src/app/page.tsx
109
- // ⚠ ./src/utils/get-email-component.tsx
110
- // Critical dependency: the request of a dependency is an expression
111
- },
112
- process,
113
- };
114
70
  const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
115
71
  // because it will have a path like <tsconfigLocation>/stdout/email.js.map
116
72
  sourceMapToEmail.sourceRoot = path.resolve(sourceMapFile.path, '../..');
117
73
  sourceMapToEmail.sources = sourceMapToEmail.sources.map((source) =>
118
74
  path.resolve(sourceMapFile.path, '..', source),
119
75
  );
120
- try {
121
- vm.runInNewContext(builtEmailCode, fakeContext, { filename: emailPath });
122
- } catch (exception) {
123
- const error = exception as Error;
124
76
 
125
- error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
77
+ const runningResult = runBundledCode(builtEmailCode, emailPath);
126
78
 
127
- return {
128
- error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
129
- };
79
+ if (isErr(runningResult)) {
80
+ const { error } = runningResult;
81
+ if (error instanceof Error) {
82
+ error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
83
+
84
+ return {
85
+ error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
86
+ };
87
+ }
88
+
89
+ throw error;
130
90
  }
131
91
 
132
- if (fakeContext.module.exports.default === undefined) {
92
+ const parseResult = EmailComponentModule.safeParse(runningResult.value);
93
+
94
+ if (parseResult.error) {
133
95
  return {
134
96
  error: improveErrorWithSourceMap(
135
97
  new Error(
136
- `The email component at ${emailPath} does not contain a default export`,
98
+ `The email component at ${emailPath} does not contain the expected exports`,
99
+ {
100
+ cause: parseResult.error,
101
+ },
137
102
  ),
138
103
  emailPath,
139
104
  sourceMapToEmail,
@@ -141,11 +106,13 @@ export const getEmailComponent = async (
141
106
  };
142
107
  }
143
108
 
109
+ const { data: componentModule } = parseResult;
110
+
144
111
  return {
145
- emailComponent: fakeContext.module.exports.default as EmailComponent,
146
- render: fakeContext.module.exports.render as typeof render,
147
- createElement: fakeContext.module.exports
148
- .reactEmailCreateReactElement as typeof React.createElement,
112
+ emailComponent: componentModule.default as EmailComponent,
113
+ render: componentModule.render as typeof render,
114
+ createElement:
115
+ componentModule.reactEmailCreateReactElement as typeof React.createElement,
149
116
 
150
117
  sourceMapToOriginalFile: sourceMapToEmail,
151
118
  };
@@ -0,0 +1,85 @@
1
+ import { checkCompatibility } from '../actions/email-validation/check-compatibility';
2
+ import { checkImages } from '../actions/email-validation/check-images';
3
+ import { checkLinks } from '../actions/email-validation/check-links';
4
+ import type { LintingRow } from '../components/toolbar/linter';
5
+
6
+ export interface LintingSource<T> {
7
+ getStream(): Promise<ReadableStream<T>>;
8
+ mapValue(value: NoInfer<T>): LintingRow | undefined;
9
+ }
10
+
11
+ function createSource<T>(source: LintingSource<T>): LintingSource<T> {
12
+ return source;
13
+ }
14
+
15
+ export function getLintingSources(
16
+ markup: string,
17
+ reactMarkup: string,
18
+ emailPath: string,
19
+
20
+ urlBase: string,
21
+ ): LintingSource<unknown>[] {
22
+ return [
23
+ createSource({
24
+ getStream() {
25
+ return checkImages(markup, urlBase);
26
+ },
27
+ mapValue(result) {
28
+ if (result && result.status !== 'success') {
29
+ return {
30
+ result: result,
31
+ source: 'image',
32
+ };
33
+ }
34
+ },
35
+ }),
36
+ createSource({
37
+ getStream() {
38
+ return checkLinks(markup);
39
+ },
40
+ mapValue(result) {
41
+ if (result && result.status !== 'success') {
42
+ return {
43
+ result: result,
44
+ source: 'link',
45
+ };
46
+ }
47
+ },
48
+ }),
49
+ createSource({
50
+ getStream() {
51
+ return checkCompatibility(reactMarkup, emailPath);
52
+ },
53
+ mapValue(value) {
54
+ if (value && value.status !== 'success') {
55
+ return {
56
+ result: value,
57
+ source: 'compatibility',
58
+ };
59
+ }
60
+ },
61
+ }),
62
+ ];
63
+ }
64
+
65
+ export async function* loadLintingRowsFrom(sources: LintingSource<unknown>[]) {
66
+ for await (const source of sources) {
67
+ const stream = await source.getStream();
68
+ const reader = stream.getReader();
69
+ try {
70
+ while (true) {
71
+ const { value, done } = await reader.read();
72
+ if (done) {
73
+ break;
74
+ }
75
+
76
+ const row = source.mapValue(value);
77
+ if (row) {
78
+ yield row;
79
+ }
80
+ }
81
+ } finally {
82
+ reader.releaseLock();
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,49 @@
1
+ type Ok<T, _E> = {
2
+ value: T;
3
+ };
4
+ type Error<_T, E> = {
5
+ error: E;
6
+ };
7
+
8
+ /**
9
+ * Do not destructure this object, it is meant to have all fields together
10
+ * in the same object
11
+ */
12
+ export type Result<T, E> = Ok<T, E> | Error<T, E>;
13
+
14
+ export function isErr<T, E>(result: Result<T, E>): result is Error<T, E> {
15
+ return 'error' in result;
16
+ }
17
+
18
+ export function isOk<T, E>(result: Result<T, E>): result is Ok<T, E> {
19
+ return 'value' in result && !('error' in result);
20
+ }
21
+
22
+ export function mapResult<T, E, B>(
23
+ result: Result<T, E>,
24
+ callback: (value: T) => B,
25
+ ): Result<B, E> {
26
+ if (isOk(result)) {
27
+ return ok(callback(result.value));
28
+ }
29
+
30
+ return result;
31
+ }
32
+
33
+ export function ok<T, E>(value: NoInfer<T>): Ok<T, E>;
34
+ // biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error
35
+ export function ok<T extends void = void, E = never>(value: void): Ok<void, E>;
36
+ export function ok<T, E>(value: NoInfer<T>): Ok<T, E> {
37
+ return {
38
+ value,
39
+ };
40
+ }
41
+
42
+ export function err<T, E>(error: NoInfer<E>): Error<T, E>;
43
+ // biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error
44
+ export function err<T, E extends void = void>(error: void): Error<T, void>;
45
+ export function err<T, E>(error: NoInfer<E>): Error<T, E> {
46
+ return {
47
+ error,
48
+ };
49
+ }
@@ -0,0 +1,64 @@
1
+ import path from 'node:path';
2
+ import vm from 'node:vm';
3
+ import { type Result, err, ok } from './result';
4
+ import { staticNodeModulesForVM } from './static-node-modules-for-vm';
5
+
6
+ export const runBundledCode = (
7
+ code: string,
8
+ filename: string,
9
+ ): Result<unknown, unknown> => {
10
+ const fakeContext = {
11
+ ...global,
12
+ console,
13
+ Buffer,
14
+ AbortSignal,
15
+ Event,
16
+ EventTarget,
17
+ TextDecoder,
18
+ Request,
19
+ Response,
20
+ TextDecoderStream,
21
+ TextEncoder,
22
+ TextEncoderStream,
23
+ ReadableStream,
24
+ URL,
25
+ URLSearchParams,
26
+ Headers,
27
+ module: {
28
+ exports: {},
29
+ },
30
+ __filename: filename,
31
+ __dirname: path.dirname(filename),
32
+ require: (specifiedModule: string) => {
33
+ let m = specifiedModule;
34
+ if (specifiedModule.startsWith('node:')) {
35
+ m = m.split(':')[1]!;
36
+ }
37
+
38
+ if (m in staticNodeModulesForVM) {
39
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
40
+ return staticNodeModulesForVM[m];
41
+ }
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
44
+ return require(`${specifiedModule}`) as unknown;
45
+ // this stupid string templating was necessary to not have
46
+ // webpack warnings like:
47
+ //
48
+ // Import trace for requested module:
49
+ // ./src/utils/get-email-component.tsx
50
+ // ./src/app/page.tsx
51
+ // ⚠ ./src/utils/get-email-component.tsx
52
+ // Critical dependency: the request of a dependency is an expression
53
+ },
54
+ process,
55
+ };
56
+
57
+ try {
58
+ vm.runInNewContext(code, fakeContext, { filename });
59
+ } catch (exception) {
60
+ return err(exception);
61
+ }
62
+
63
+ return ok(fakeContext.module.exports as unknown);
64
+ };