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

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 (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cli/index.js +17 -16
  3. package/dist/cli/index.mjs +25 -24
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +5 -5
  6. package/dist/preview/.next/app-path-routes-manifest.json +1 -1
  7. package/dist/preview/.next/build-manifest.json +2 -2
  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/next-minimal-server.js.nft.json +1 -1
  15. package/dist/preview/.next/next-server.js.nft.json +1 -1
  16. package/dist/preview/.next/prerender-manifest.json +3 -3
  17. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/preview/.next/server/app/page.js +1 -1
  20. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  21. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  22. package/dist/preview/.next/server/app/preview/[...slug]/page.js +113 -9
  23. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  24. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  25. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  26. package/dist/preview/.next/server/chunks/42.js +1 -0
  27. package/dist/preview/.next/server/pages/500.html +1 -1
  28. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  29. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  30. package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
  31. package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
  32. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
  33. package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
  34. package/dist/preview/.next/trace +26 -26
  35. package/package.json +1 -1
  36. package/src/actions/email-validation/check-compatibility.ts +16 -4
  37. package/src/components/sidebar/file-tree-directory-children.tsx +12 -2
  38. package/src/components/sidebar/file-tree-directory.tsx +26 -18
  39. package/src/components/sidebar/file-tree.tsx +2 -2
  40. package/src/components/sidebar/sidebar.tsx +16 -18
  41. package/src/components/toolbar/spam-assassin.tsx +16 -13
  42. package/src/components/toolbar/use-cached-state.ts +2 -2
  43. package/src/components/toolbar.tsx +60 -16
  44. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  45. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  46. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  47. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  48. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  49. package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
  50. package/dist/preview/.next/server/chunks/943.js +0 -1
  51. package/dist/preview/.next/static/chunks/683-1fb40795502f6e63.js +0 -1
  52. package/dist/preview/.next/static/chunks/app/layout-ffdee5cc1be30e7b.js +0 -1
  53. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9e22979a25c836c0.js +0 -1
  54. package/dist/preview/.next/static/css/eaae8ce545b295f9.css +0 -3
  55. /package/dist/preview/.next/static/{Pms2orsQgT5xpttCfZfH5 → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
  56. /package/dist/preview/.next/static/{Pms2orsQgT5xpttCfZfH5 → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.0.0-alpha.7",
3
+ "version": "4.0.0-alpha.8",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -154,7 +154,11 @@ export const checkCompatibility = async (
154
154
  );
155
155
  if (Object.keys(compatibilityStats.perEmailClient).length === 0)
156
156
  continue;
157
- if (compatibilityStats.status === 'success') continue;
157
+ if (
158
+ compatibilityStats.status === 'success' ||
159
+ compatibilityStats.status === 'warning'
160
+ )
161
+ continue;
158
162
 
159
163
  if (entry.category === 'html') {
160
164
  const entryElements = getElementNames(entry.title, entry.keywords);
@@ -274,8 +278,9 @@ export const checkCompatibility = async (
274
278
 
275
279
  if (cssEntryType === 'full property') {
276
280
  if (
277
- property.name === entryFullProperty?.name &&
278
- property.value === entryFullProperty.value
281
+ snakeToCamel(property.name) ===
282
+ snakeToCamel(entryFullProperty!.name) &&
283
+ property.value === entryFullProperty!.value
279
284
  ) {
280
285
  addToInsights(property);
281
286
  break;
@@ -302,7 +307,8 @@ export const checkCompatibility = async (
302
307
  }
303
308
  } else if (
304
309
  entryProperties.some(
305
- (propertyName) => property.name === propertyName,
310
+ (propertyName) =>
311
+ snakeToCamel(property.name) === snakeToCamel(propertyName),
306
312
  )
307
313
  ) {
308
314
  addToInsights(property);
@@ -318,4 +324,10 @@ export const checkCompatibility = async (
318
324
  return readableStream;
319
325
  };
320
326
 
327
+ const snakeToCamel = (snakeStr: string) => {
328
+ return snakeStr
329
+ .toLowerCase()
330
+ .replace(/-+([a-z])/g, (_match, letter) => letter.toUpperCase());
331
+ };
332
+
321
333
  export type AST = ReturnType<typeof parse>;
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
5
5
  import { cn } from '../../utils';
6
6
  import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
7
7
  import { IconFile } from '../icons/icon-file';
8
+ import { Tooltip } from '../tooltip';
8
9
  import { FileTreeDirectory } from './file-tree-directory';
9
10
 
10
11
  export const FileTreeDirectoryChildren = (props: {
@@ -77,7 +78,7 @@ export const FileTreeDirectoryChildren = (props: {
77
78
  <motion.span
78
79
  animate={{ x: 0, opacity: 1 }}
79
80
  className={cn(
80
- 'relative flex h-8 max-w-full items-center gap-2 rounded-md align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
81
+ 'relative flex h-8 w-full items-center text-start gap-2 rounded-md align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
81
82
  props.isRoot ? undefined : 'pl-3',
82
83
  {
83
84
  'text-cyan-11': isCurrentPage,
@@ -116,7 +117,16 @@ export const FileTreeDirectoryChildren = (props: {
116
117
  height="20"
117
118
  width="20"
118
119
  />
119
- <span className="truncate">{emailFilename}</span>
120
+ <Tooltip.Provider>
121
+ <Tooltip>
122
+ <Tooltip.Trigger asChild>
123
+ <span className="truncate w-[calc(100%-1.25rem)]">
124
+ {emailFilename}
125
+ </span>
126
+ </Tooltip.Trigger>
127
+ <Tooltip.Content>{emailFilename}</Tooltip.Content>
128
+ </Tooltip>
129
+ </Tooltip.Provider>
120
130
  </motion.span>
121
131
  </Link>
122
132
  );
@@ -7,6 +7,7 @@ import { Heading } from '../heading';
7
7
  import { IconArrowDown } from '../icons/icon-arrow-down';
8
8
  import { IconFolder } from '../icons/icon-folder';
9
9
  import { IconFolderOpen } from '../icons/icon-folder-open';
10
+ import { Tooltip } from '../tooltip';
10
11
  import { FileTreeDirectoryChildren } from './file-tree-directory-children';
11
12
 
12
13
  interface SidebarDirectoryProps {
@@ -51,31 +52,38 @@ export const FileTreeDirectory = ({
51
52
  >
52
53
  <Collapsible.Trigger
53
54
  className={cn(
54
- 'mt-1 mb-1.5 flex w-full items-center justify-between gap-2 font-medium text-[14px]',
55
+ 'mt-1 mb-1.5 flex w-full items-center text-start justify-between gap-2 font-medium text-[14px]',
55
56
  {
56
57
  'cursor-pointer': !isEmpty,
57
58
  },
58
59
  )}
59
60
  >
60
- <div className="flex items-center gap-2 text-slate-11 transition duration-200 ease-in-out hover:text-slate-12">
61
- {open ? (
62
- <IconFolderOpen height="20" width="20" />
63
- ) : (
64
- <IconFolder height="20" width="20" />
65
- )}
66
- <Heading
67
- as="h3"
68
- className="transition duration-200 ease-in-out hover:text-slate-12"
69
- color="gray"
70
- size="2"
71
- weight="medium"
72
- >
73
- {directoryMetadata.directoryName}
74
- </Heading>
75
- </div>
61
+ {open ? (
62
+ <IconFolderOpen className="w-[20px]" height="20" width="20" />
63
+ ) : (
64
+ <IconFolder height="20" width="20" />
65
+ )}
66
+ <Tooltip.Provider>
67
+ <Tooltip>
68
+ <Tooltip.Trigger asChild>
69
+ <Heading
70
+ as="h3"
71
+ className="transition grow w-[calc(100%-40px)] truncate duration-200 ease-in-out hover:text-slate-12"
72
+ color="gray"
73
+ size="2"
74
+ weight="medium"
75
+ >
76
+ {directoryMetadata.directoryName}
77
+ </Heading>
78
+ </Tooltip.Trigger>
79
+ <Tooltip.Content>{directoryMetadata.directoryName}</Tooltip.Content>
80
+ </Tooltip>
81
+ </Tooltip.Provider>
76
82
  {!isEmpty ? (
77
83
  <IconArrowDown
78
- className="justify-self-end opacity-60 transition-transform data-[open=true]:rotate-180"
84
+ width="20"
85
+ height="20"
86
+ className="ml-auto opacity-60 transition-transform data-[open=true]:rotate-180"
79
87
  data-open={open}
80
88
  />
81
89
  ) : null}
@@ -13,8 +13,8 @@ export const FileTree = ({
13
13
  emailsDirectoryMetadata,
14
14
  }: FileTreeProps) => {
15
15
  return (
16
- <div className="flex h-full w-full flex-col overflow-hidden lg:w-full lg:min-w-[14.5rem]">
17
- <nav className="flex w-full flex-grow flex-col overflow-y-auto p-4 pr-0 pl-0">
16
+ <div className="flex w-full h-full flex-col lg:w-full lg:min-w-[14.5rem]">
17
+ <nav className="flex flex-grow flex-col p-4 pr-0 pl-0">
18
18
  <Collapsible.Root open>
19
19
  <React.Suspense>
20
20
  <FileTreeDirectoryChildren
@@ -16,28 +16,26 @@ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
16
16
  return (
17
17
  <aside
18
18
  className={cn(
19
- 'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-screen max-w-ful overflow-hidden bg-black will-change-auto',
19
+ 'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-screen max-w-full bg-black will-change-auto',
20
20
  'lg:static lg:z-auto lg:max-h-screen lg:w-[16rem]',
21
21
  className,
22
22
  )}
23
23
  >
24
- <div className="w-full h-full overflow-y-auto overflow-x-hidden">
25
- <div className="flex w-full h-full flex-col border-slate-6 border-r">
26
- <div
27
- className={clsx(
28
- 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
29
- )}
30
- >
31
- <h2>
32
- <Logo />
33
- </h2>
34
- </div>
35
- <div className="relative h-full w-full border-slate-4 border-t px-4 pb-3">
36
- <FileTree
37
- currentEmailOpenSlug={currentEmailOpenSlug}
38
- emailsDirectoryMetadata={emailsDirectoryMetadata}
39
- />
40
- </div>
24
+ <div className="flex w-full h-full overflow-hidden flex-col border-slate-6 border-r">
25
+ <div
26
+ className={clsx(
27
+ 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
28
+ )}
29
+ >
30
+ <h2>
31
+ <Logo />
32
+ </h2>
33
+ </div>
34
+ <div className="relative grow w-full h-full overflow-y-auto overflow-x-hidden border-slate-4 border-t px-4 pb-3">
35
+ <FileTree
36
+ currentEmailOpenSlug={currentEmailOpenSlug}
37
+ emailsDirectoryMetadata={emailsDirectoryMetadata}
38
+ />
41
39
  </div>
42
40
  </div>
43
41
  </aside>
@@ -101,19 +101,22 @@ export const SpamAssassin = ({ result }: SpamAssassinProps) => {
101
101
  <Results.Column>
102
102
  {result.points === 0
103
103
  ? 'Congratulations! Your email is clean of abuse indicators.'
104
- : 'Lower scores are better'}
104
+ : 'Higher scores are better'}
105
105
  </Results.Column>
106
- <Results.Column
107
- className={cn(
108
- 'text-right text-3xl tracking-tighter font-bold',
109
- result.points === 0 ? 'text-green-400' : null,
110
- result.points > 0 && result.points <= 1.5 ? null : null,
111
- result.points > 1.5 ? 'text-yellow-200' : null,
112
- result.points > 3 ? 'text-orange-400' : null,
113
- result.points >= 5 ? 'text-red-400' : null,
114
- )}
115
- >
116
- {result.points.toFixed(1)}
106
+ <Results.Column className="text-right tracking-tighter font-bold">
107
+ <span
108
+ className={cn(
109
+ 'text-3xl',
110
+ result.points === 0 ? 'text-green-400' : null,
111
+ result.points > 0 && result.points <= 1.5 ? null : null,
112
+ result.points > 1.5 ? 'text-yellow-200' : null,
113
+ result.points > 3 ? 'text-orange-400' : null,
114
+ result.points >= 5 ? 'text-red-400' : null,
115
+ )}
116
+ >
117
+ {(10 - result.points).toFixed(1)}
118
+ </span>{' '}
119
+ <span className="text-lg">/ 10</span>
117
120
  </Results.Column>
118
121
  </Results.Row>
119
122
  {toSorted(result.checks, (a, b) => b.points - a.points).map(
@@ -140,7 +143,7 @@ export const SpamAssassin = ({ result }: SpamAssassinProps) => {
140
143
  check.points > 3 ? 'text-red-400' : null,
141
144
  )}
142
145
  >
143
- {check.points.toFixed(1)}
146
+ -{check.points.toFixed(1)}
144
147
  </Results.Column>
145
148
  </Results.Row>
146
149
  ),
@@ -4,7 +4,7 @@ export const useCachedState = <T>(key: string) => {
4
4
  let value: T | undefined = undefined;
5
5
  if ('localStorage' in global) {
6
6
  const storedValue = global.localStorage.getItem(key);
7
- if (storedValue !== null) {
7
+ if (storedValue !== null && storedValue !== 'undefined') {
8
8
  try {
9
9
  value = JSON.parse(storedValue) as T;
10
10
  } catch (exception) {
@@ -12,7 +12,7 @@ export const useCachedState = <T>(key: string) => {
12
12
  'Failed to load stored value for',
13
13
  key,
14
14
  'with value',
15
- value,
15
+ storedValue,
16
16
  );
17
17
  }
18
18
  }
@@ -8,6 +8,7 @@ import { isBuilding } from '../app/env';
8
8
  import { PreviewContext } from '../contexts/preview';
9
9
  import { cn } from '../utils';
10
10
  import { IconArrowDown } from './icons/icon-arrow-down';
11
+ import { IconCheck } from './icons/icon-check';
11
12
  import { IconInfo } from './icons/icon-info';
12
13
  import { IconReload } from './icons/icon-reload';
13
14
  import { Compatibility, useCompatibility } from './toolbar/compatibility';
@@ -155,13 +156,6 @@ const ToolbarInner = ({
155
156
  'The Compatibility tab shows how well the HTML/CSS is supported across mail clients like Outlook, Gmail, etc. Powered by Can I Email.') ||
156
157
  'Info'
157
158
  }
158
- onClick={() => {
159
- if (activeTab === undefined) {
160
- setActivePanelValue('linter');
161
- } else {
162
- setActivePanelValue(undefined);
163
- }
164
- }}
165
159
  >
166
160
  <IconInfo size={24} />
167
161
  </ToolbarButton>
@@ -215,26 +209,50 @@ const ToolbarInner = ({
215
209
  <div className="animate-pulse text-slate-11 text-sm pt-1">
216
210
  Running linting...
217
211
  </div>
212
+ ) : lintingRows?.length === 0 ? (
213
+ <div className="flex flex-col items-center justify-center pt-8">
214
+ <SuccessIcon />
215
+ <SuccessTitle>All good</SuccessTitle>
216
+ <SuccessDescription>
217
+ No linting issues found.
218
+ </SuccessDescription>
219
+ </div>
218
220
  ) : (
219
- <Linter rows={lintingRows} />
221
+ <Linter rows={lintingRows ?? []} />
220
222
  )}
221
223
  </Tabs.Content>
222
- <Tabs.Content value="spam-assassin">
223
- {spamLoading ? (
224
+ <Tabs.Content value="compatibility">
225
+ {compatibilityLoading ? (
224
226
  <div className="animate-pulse text-slate-11 text-sm pt-1">
225
- Running spam check...
227
+ Running compatibility check...
228
+ </div>
229
+ ) : compatibilityCheckingResults?.length === 0 ? (
230
+ <div className="flex flex-col items-center justify-center py-8 px-4 my-4">
231
+ <SuccessIcon />
232
+ <SuccessTitle>Great compatibility</SuccessTitle>
233
+ <SuccessDescription>
234
+ It should render properly everywhere.
235
+ </SuccessDescription>
226
236
  </div>
227
237
  ) : (
228
- <SpamAssassin result={spamCheckingResult} />
238
+ <Compatibility results={compatibilityCheckingResults ?? []} />
229
239
  )}
230
240
  </Tabs.Content>
231
- <Tabs.Content value="compatibility">
232
- {compatibilityLoading ? (
241
+ <Tabs.Content value="spam-assassin">
242
+ {spamLoading ? (
233
243
  <div className="animate-pulse text-slate-11 text-sm pt-1">
234
- Running compatibility check...
244
+ Running spam check...
245
+ </div>
246
+ ) : spamCheckingResult?.isSpam === false ? (
247
+ <div className="flex flex-col items-center justify-center py-4 px-4 my-4">
248
+ <SuccessIcon />
249
+ <SuccessTitle>10/10</SuccessTitle>
250
+ <SuccessDescription>
251
+ Your email is clean of abuse indicators.
252
+ </SuccessDescription>
235
253
  </div>
236
254
  ) : (
237
- <Compatibility results={compatibilityCheckingResults} />
255
+ <SpamAssassin result={spamCheckingResult} />
238
256
  )}
239
257
  </Tabs.Content>
240
258
  </div>
@@ -244,6 +262,32 @@ const ToolbarInner = ({
244
262
  );
245
263
  };
246
264
 
265
+ const SuccessIcon = () => {
266
+ return (
267
+ <div className="relative mb-8 flex items-center justify-center">
268
+ <div className="h-16 w-16 rounded-full bg-gradient-to-br from-green-300/20 opacity-80 to-emerald-500/30 blur-md absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
269
+ <div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-400/80 opacity-10 to-emerald-600/80 absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-lg" />
270
+ <div className="h-10 w-10 rounded-full bg-gradient-to-br from-green-400 to-emerald-600 flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-[inset_0_1px_1px_rgba(255,255,255,0.4)]">
271
+ <IconCheck size={24} className="text-white drop-shadow-sm" />
272
+ </div>
273
+ </div>
274
+ );
275
+ };
276
+
277
+ const SuccessTitle = ({ children }) => {
278
+ return (
279
+ <h3 className="text-slate-12 font-medium text-base mb-1">{children}</h3>
280
+ );
281
+ };
282
+
283
+ const SuccessDescription = ({ children }) => {
284
+ return (
285
+ <p className="text-slate-11 text-sm text-center max-w-[300px]">
286
+ {children}
287
+ </p>
288
+ );
289
+ };
290
+
247
291
  interface ToolbarProps {
248
292
  serverSpamCheckingResult: SpamCheckingResult | undefined;
249
293
  serverLintingRows: LintingRow[] | undefined;
@@ -1,3 +1,3 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com/teams/invite/foo</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
3
+ exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel Logo" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img alt="alanturing&#x27;s profile picture" height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="Arrow indicating invitation" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img alt="Enigma team logo" height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
@@ -0,0 +1,74 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`getObjectVariables() 1`] = `
4
+ {
5
+ "buttonStyle": [
6
+ Node {
7
+ "computed": false,
8
+ "end": 91,
9
+ "key": Node {
10
+ "end": 84,
11
+ "loc": SourceLocation {
12
+ "end": Position {
13
+ "column": 14,
14
+ "index": 84,
15
+ "line": 5,
16
+ },
17
+ "filename": undefined,
18
+ "identifierName": "borderRadius",
19
+ "start": Position {
20
+ "column": 2,
21
+ "index": 72,
22
+ "line": 5,
23
+ },
24
+ },
25
+ "name": "borderRadius",
26
+ "start": 72,
27
+ "type": "Identifier",
28
+ },
29
+ "loc": SourceLocation {
30
+ "end": Position {
31
+ "column": 21,
32
+ "index": 91,
33
+ "line": 5,
34
+ },
35
+ "filename": undefined,
36
+ "identifierName": undefined,
37
+ "start": Position {
38
+ "column": 2,
39
+ "index": 72,
40
+ "line": 5,
41
+ },
42
+ },
43
+ "method": false,
44
+ "shorthand": false,
45
+ "start": 72,
46
+ "type": "ObjectProperty",
47
+ "value": Node {
48
+ "end": 91,
49
+ "extra": {
50
+ "raw": "'5px'",
51
+ "rawValue": "5px",
52
+ },
53
+ "loc": SourceLocation {
54
+ "end": Position {
55
+ "column": 21,
56
+ "index": 91,
57
+ "line": 5,
58
+ },
59
+ "filename": undefined,
60
+ "identifierName": undefined,
61
+ "start": Position {
62
+ "column": 16,
63
+ "index": 86,
64
+ "line": 5,
65
+ },
66
+ },
67
+ "start": 86,
68
+ "type": "StringLiteral",
69
+ "value": "5px",
70
+ },
71
+ },
72
+ ],
73
+ }
74
+ `;
@@ -0,0 +1,24 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`getUsedStyleProperties() 1`] = `
4
+ [
5
+ {
6
+ "location": SourceLocation {
7
+ "end": Position {
8
+ "column": 21,
9
+ "index": 91,
10
+ "line": 5,
11
+ },
12
+ "filename": undefined,
13
+ "identifierName": undefined,
14
+ "start": Position {
15
+ "column": 2,
16
+ "index": 72,
17
+ "line": 5,
18
+ },
19
+ },
20
+ "name": "borderRadius",
21
+ "value": "5px",
22
+ },
23
+ ]
24
+ `;
@@ -0,0 +1,19 @@
1
+ import { parse } from '@babel/parser';
2
+ import { getObjectVariables } from './get-object-variables';
3
+
4
+ test('getObjectVariables()', () => {
5
+ const reactCode = `
6
+ <Button style={buttonStyle}>Click me</Button>
7
+
8
+ const buttonStyle = {
9
+ borderRadius: '5px',
10
+ };
11
+ `;
12
+ const ast = parse(reactCode, {
13
+ strictMode: false,
14
+ errorRecovery: true,
15
+ sourceType: 'unambiguous',
16
+ plugins: ['jsx', 'typescript', 'decorators'],
17
+ });
18
+ expect(getObjectVariables(ast)).toMatchSnapshot();
19
+ });
@@ -0,0 +1,23 @@
1
+ import { parse } from '@babel/parser';
2
+ import { getObjectVariables } from './get-object-variables';
3
+ import { getUsedStyleProperties } from './get-used-style-properties';
4
+
5
+ test('getUsedStyleProperties()', async () => {
6
+ const reactCode = `
7
+ <Button style={buttonStyle}>Click me</Button>
8
+
9
+ const buttonStyle = {
10
+ borderRadius: '5px',
11
+ };
12
+ `;
13
+ const ast = parse(reactCode, {
14
+ strictMode: false,
15
+ errorRecovery: true,
16
+ sourceType: 'unambiguous',
17
+ plugins: ['jsx', 'typescript', 'decorators'],
18
+ });
19
+ const objectVariables = getObjectVariables(ast);
20
+ expect(
21
+ await getUsedStyleProperties(ast, reactCode, '', objectVariables),
22
+ ).toMatchSnapshot();
23
+ });
@@ -6,8 +6,8 @@ export const getCssPropertyWithValue = (title: string) => {
6
6
  if (match) {
7
7
  const [_full, propertyName, propertyValue] = match;
8
8
  return {
9
- name: propertyName,
10
- value: propertyValue,
9
+ name: propertyName!,
10
+ value: propertyValue!,
11
11
  };
12
12
  }
13
13
  return undefined;