react-email 2.0.0-canary.0 → 2.0.0-canary.2

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.
package/cli/index.js CHANGED
@@ -252,7 +252,7 @@ var import_extra_typings = require("@commander-js/extra-typings");
252
252
  // package.json
253
253
  var package_default = {
254
254
  name: "react-email",
255
- version: "2.0.0-canary.0",
255
+ version: "2.0.0-canary.2",
256
256
  description: "A live preview of your emails right in your browser.",
257
257
  bin: {
258
258
  email: "./cli/index.js"
@@ -296,7 +296,7 @@ var package_default = {
296
296
  clsx: "2.1.0",
297
297
  commander: "9.4.1",
298
298
  debounce: "2.0.0",
299
- esbuild: "0.16.4",
299
+ esbuild: "0.19.11",
300
300
  "eslint-config-prettier": "9.0.0",
301
301
  "eslint-config-turbo": "1.10.12",
302
302
  "fast-glob": "3.3.2",
@@ -571,6 +571,22 @@ var startDevServer = function() {
571
571
  startDevServer(emailsDirRelativePath, staticBaseDirRelativePath, nextPortToTry)
572
572
  ];
573
573
  }
574
+ devServer.on("close", /*#__PURE__*/ _async_to_generator(function() {
575
+ return _ts_generator(this, function(_state) {
576
+ switch(_state.label){
577
+ case 0:
578
+ return [
579
+ 4,
580
+ app.close()
581
+ ];
582
+ case 1:
583
+ _state.sent();
584
+ return [
585
+ 2
586
+ ];
587
+ }
588
+ });
589
+ }));
574
590
  devServer.on("error", function(e) {
575
591
  console.error(" ".concat(import_log_symbols.default.error, " preview server error: "), JSON.stringify(e));
576
592
  process.exit(1);
package/cli/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { program } from "@commander-js/extra-typings";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "react-email",
9
- version: "2.0.0-canary.0",
9
+ version: "2.0.0-canary.2",
10
10
  description: "A live preview of your emails right in your browser.",
11
11
  bin: {
12
12
  email: "./cli/index.js"
@@ -50,7 +50,7 @@ var package_default = {
50
50
  clsx: "2.1.0",
51
51
  commander: "9.4.1",
52
52
  debounce: "2.0.0",
53
- esbuild: "0.16.4",
53
+ esbuild: "0.19.11",
54
54
  "eslint-config-prettier": "9.0.0",
55
55
  "eslint-config-turbo": "1.10.12",
56
56
  "fast-glob": "3.3.2",
@@ -275,6 +275,9 @@ var startDevServer = async (emailsDirRelativePath, staticBaseDirRelativePath, po
275
275
  nextPortToTry
276
276
  );
277
277
  }
278
+ devServer.on("close", async () => {
279
+ await app.close();
280
+ });
278
281
  devServer.on("error", (e) => {
279
282
  console.error(
280
283
  ` ${logSymbols.error} preview server error: `,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "2.0.0-canary.0",
3
+ "version": "2.0.0-canary.2",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./cli/index.js"
@@ -38,7 +38,7 @@
38
38
  "clsx": "2.1.0",
39
39
  "commander": "9.4.1",
40
40
  "debounce": "2.0.0",
41
- "esbuild": "0.16.4",
41
+ "esbuild": "0.19.11",
42
42
  "eslint-config-prettier": "9.0.0",
43
43
  "eslint-config-turbo": "1.10.12",
44
44
  "fast-glob": "3.3.2",
@@ -5,6 +5,7 @@ import { renderAsync } from '@react-email/render';
5
5
  import { getEmailComponent } from '../utils/get-email-component';
6
6
  import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
7
7
  import type { ErrorObject } from '../utils/types/error-object';
8
+ import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
8
9
 
9
10
  export interface RenderedEmailMetadata {
10
11
  markup: string;
@@ -29,22 +30,34 @@ export const renderEmailBySlug = async (
29
30
  return { error: result.error };
30
31
  }
31
32
 
32
- const Email = result.emailComponent;
33
+ const { emailComponent: Email, sourceMapToOriginalFile } = result;
33
34
 
34
35
  const previewProps = Email.PreviewProps || {};
35
36
  const EmailComponent = Email as React.FC;
36
- const markup = await renderAsync(<EmailComponent {...previewProps} />, {
37
- pretty: true,
38
- });
39
- const plainText = await renderAsync(<EmailComponent {...previewProps} />, {
40
- plainText: true,
41
- });
42
-
43
- const reactMarkup = await fs.readFile(emailPath, 'utf-8');
44
-
45
- return {
46
- markup,
47
- plainText,
48
- reactMarkup,
49
- };
37
+ try {
38
+ const markup = await renderAsync(<EmailComponent {...previewProps} />, {
39
+ pretty: true,
40
+ });
41
+ const plainText = await renderAsync(<EmailComponent {...previewProps} />, {
42
+ plainText: true,
43
+ });
44
+
45
+ const reactMarkup = await fs.readFile(emailPath, 'utf-8');
46
+
47
+ return {
48
+ markup,
49
+ plainText,
50
+ reactMarkup,
51
+ };
52
+ } catch (exception) {
53
+ const error = exception as Error;
54
+
55
+ return {
56
+ error: improveErrorWithSourceMap(
57
+ error,
58
+ emailPath,
59
+ sourceMapToOriginalFile,
60
+ ),
61
+ };
62
+ }
50
63
  };
@@ -101,7 +101,6 @@ const Preview = ({
101
101
  <iframe
102
102
  className="w-full h-[calc(100vh_-_140px)]"
103
103
  srcDoc={renderedEmailMetadata.markup}
104
- // @ts-expect-error For some reason the title prop is not included with the iframe
105
104
  title={slug}
106
105
  />
107
106
  ) : (
@@ -17,6 +17,7 @@ export const SidebarDirectoryChildren = (props: {
17
17
  }) => {
18
18
  const directoryPathRelativeToEmailsDirectory =
19
19
  props.emailsDirectoryMetadata.absolutePath
20
+ .replace(`${emailsDirectoryAbsolutePath}${pathSeparator}`, '')
20
21
  .replace(emailsDirectoryAbsolutePath, '')
21
22
  .trim();
22
23
  const isBaseEmailsDirectory =
@@ -2,7 +2,10 @@
2
2
  import * as Collapsible from '@radix-ui/react-collapsible';
3
3
  import * as React from 'react';
4
4
  import { cn } from '../../utils';
5
- import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
5
+ import {
6
+ emailsDirectoryAbsolutePath,
7
+ pathSeparator,
8
+ } from '../../utils/emails-directory-absolute-path';
6
9
  import { type EmailsDirectory } from '../../actions/get-emails-directory-metadata';
7
10
  import { Heading } from '../heading';
8
11
  import { IconFolder } from '../icons/icon-folder';
@@ -15,34 +18,47 @@ interface SidebarDirectoryProps {
15
18
  currentEmailOpenSlug?: string;
16
19
  }
17
20
 
21
+ const persistedOpenDirectories = new Set<string>();
22
+
18
23
  export const SidebarDirectory = ({
19
- emailsDirectoryMetadata,
24
+ emailsDirectoryMetadata: directoryMetadata,
20
25
  className,
21
26
  currentEmailOpenSlug,
22
27
  }: SidebarDirectoryProps) => {
23
28
  const isBaseEmailsDirectory =
24
- emailsDirectoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
25
- const directoryPathRelativeToEmailsDirectory =
26
- emailsDirectoryMetadata.absolutePath
29
+ directoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
30
+ const directoryPathRelativeToBaseEmailsDirectory =
31
+ directoryMetadata.absolutePath
32
+ .replace(`${emailsDirectoryAbsolutePath}${pathSeparator}`, '')
27
33
  .replace(emailsDirectoryAbsolutePath, '')
28
34
  .trim();
29
- const doesFolderContainCurrentEmailOpen = currentEmailOpenSlug
30
- ? currentEmailOpenSlug.includes(directoryPathRelativeToEmailsDirectory)
35
+ const doesDirectoryContainCurrentEmailOpen = currentEmailOpenSlug
36
+ ? currentEmailOpenSlug.includes(directoryPathRelativeToBaseEmailsDirectory)
31
37
  : false;
32
38
 
33
39
  const isEmpty =
34
- emailsDirectoryMetadata.emailFilenames.length > 0 ||
35
- emailsDirectoryMetadata.subDirectories.length > 0;
40
+ directoryMetadata.emailFilenames.length > 0 ||
41
+ directoryMetadata.subDirectories.length > 0;
36
42
 
37
43
  const [open, setOpen] = React.useState(
38
- isBaseEmailsDirectory || doesFolderContainCurrentEmailOpen,
44
+ persistedOpenDirectories.has(directoryMetadata.absolutePath) ||
45
+ isBaseEmailsDirectory ||
46
+ doesDirectoryContainCurrentEmailOpen,
39
47
  );
40
48
 
41
49
  return (
42
50
  <Collapsible.Root
43
51
  className={cn('group', className)}
44
52
  data-root={isBaseEmailsDirectory}
45
- onOpenChange={setOpen}
53
+ onOpenChange={(isOpening) => {
54
+ if (isOpening) {
55
+ persistedOpenDirectories.add(directoryMetadata.absolutePath);
56
+ } else {
57
+ persistedOpenDirectories.delete(directoryMetadata.absolutePath);
58
+ }
59
+
60
+ setOpen(isOpening);
61
+ }}
46
62
  open={open}
47
63
  >
48
64
  <Collapsible.Trigger
@@ -60,7 +76,7 @@ export const SidebarDirectory = ({
60
76
  size="2"
61
77
  weight="medium"
62
78
  >
63
- {emailsDirectoryMetadata.directoryName}
79
+ {directoryMetadata.directoryName}
64
80
  </Heading>
65
81
  {isEmpty ? (
66
82
  <IconArrowDown
@@ -74,7 +90,7 @@ export const SidebarDirectory = ({
74
90
  {isEmpty ? (
75
91
  <SidebarDirectoryChildren
76
92
  currentEmailOpenSlug={currentEmailOpenSlug}
77
- emailsDirectoryMetadata={emailsDirectoryMetadata}
93
+ emailsDirectoryMetadata={directoryMetadata}
78
94
  open={open}
79
95
  />
80
96
  ) : null}
@@ -17,9 +17,9 @@ export const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(
17
17
 
18
18
  return (
19
19
  <aside className={className} ref={forwardedRef} {...props}>
20
- <nav className="p-6 w-screen md:w-full md:min-w-[275px] md:max-w-[275px] flex flex-col gap-4 border-r border-slate-6">
20
+ <nav className="p-6 h-full w-screen md:w-full md:min-w-[275px] md:max-w-[275px] flex flex-col gap-4 border-r border-slate-6">
21
21
  <SidebarDirectory
22
- className="w-fit overflow-x-auto"
22
+ className="min-w-full w-fit overflow-x-auto"
23
23
  currentEmailOpenSlug={currentEmailOpenSlug}
24
24
  emailsDirectoryMetadata={emailsDirectoryMetadata}
25
25
  />
@@ -3,17 +3,19 @@
3
3
  import vm from 'node:vm';
4
4
  import stream from 'node:stream';
5
5
  import util from 'node:util';
6
- import * as stackTraceParser from 'stacktrace-parser';
7
- import { SourceMapConsumer, type RawSourceMap } from 'source-map-js';
6
+ import { type RawSourceMap } from 'source-map-js';
8
7
  import { type OutputFile, build, type BuildFailure } from 'esbuild';
9
8
  import type { EmailTemplate as EmailComponent } from './types/email-template';
10
9
  import type { ErrorObject } from './types/error-object';
10
+ import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
11
11
 
12
12
  export const getEmailComponent = async (
13
13
  emailPath: string,
14
14
  ): Promise<
15
15
  | {
16
16
  emailComponent: EmailComponent;
17
+
18
+ sourceMapToOriginalFile: RawSourceMap;
17
19
  }
18
20
  | { error: ErrorObject }
19
21
  > => {
@@ -25,7 +27,12 @@ export const getEmailComponent = async (
25
27
  platform: 'node',
26
28
  write: false,
27
29
  format: 'cjs',
28
- jsx: 'transform',
30
+ jsx: 'automatic',
31
+ logLevel: 'silent',
32
+ // allows for using jsx on a .js file
33
+ loader: {
34
+ '.js': 'jsx',
35
+ },
29
36
  outdir: 'stdout', // just a stub for esbuild, it won't actually write to this folder
30
37
  sourcemap: 'external',
31
38
  });
@@ -75,59 +82,19 @@ export const getEmailComponent = async (
75
82
  },
76
83
  process,
77
84
  };
85
+ const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
78
86
  try {
79
87
  vm.runInNewContext(builtEmailCode, fakeContext, { filename: emailPath });
80
88
  } catch (exception) {
81
89
  const error = exception as Error;
82
- let stack: string | undefined;
83
-
84
- if (typeof error.stack !== 'undefined') {
85
- const parsedStack = stackTraceParser.parse(error.stack);
86
- const sourceMapConsumer = new SourceMapConsumer(
87
- JSON.parse(sourceMapFile.text) as RawSourceMap,
88
- );
89
- const newStackLines = [] as string[];
90
- for (const stackFrame of parsedStack) {
91
- if (stackFrame.file === emailPath) {
92
- if (stackFrame.column || stackFrame.lineNumber) {
93
- const positionWithError = sourceMapConsumer.originalPositionFor({
94
- column: stackFrame.column ?? 0,
95
- line: stackFrame.lineNumber ?? 0,
96
- });
97
- const columnAndLine =
98
- positionWithError.column && positionWithError.line
99
- ? `${positionWithError.line}:${positionWithError.column}`
100
- : positionWithError.line;
101
- newStackLines.push(
102
- ` at ${stackFrame.methodName} (${emailPath}:${columnAndLine})`,
103
- );
104
- } else {
105
- newStackLines.push(` at ${stackFrame.methodName} (${emailPath})`);
106
- }
107
- } else {
108
- const columnAndLine =
109
- stackFrame.column && stackFrame.lineNumber
110
- ? `${stackFrame.lineNumber}:${stackFrame.column}`
111
- : stackFrame.lineNumber;
112
- newStackLines.push(
113
- ` at ${stackFrame.methodName} (${stackFrame.file}:${columnAndLine})`,
114
- );
115
- }
116
- }
117
- stack = newStackLines.join('\n');
118
- }
119
90
 
120
91
  return {
121
- error: {
122
- name: error.name,
123
- message: error.message,
124
- cause: error.cause,
125
- stack,
126
- },
92
+ error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
127
93
  };
128
94
  }
129
95
 
130
96
  return {
131
97
  emailComponent: fakeContext.module.exports.default as EmailComponent,
98
+ sourceMapToOriginalFile: sourceMapToEmail,
132
99
  };
133
100
  };
@@ -0,0 +1,55 @@
1
+ import * as stackTraceParser from 'stacktrace-parser';
2
+ import { SourceMapConsumer, type RawSourceMap } from 'source-map-js';
3
+ import type { ErrorObject } from './types/error-object';
4
+
5
+ export const improveErrorWithSourceMap = (
6
+ error: Error,
7
+
8
+ originalFilePath: string,
9
+ sourceMapToOriginalFile: RawSourceMap,
10
+ ): ErrorObject => {
11
+ let stack: string | undefined;
12
+
13
+ if (typeof error.stack !== 'undefined') {
14
+ const parsedStack = stackTraceParser.parse(error.stack);
15
+ const sourceMapConsumer = new SourceMapConsumer(sourceMapToOriginalFile);
16
+ const newStackLines = [] as string[];
17
+ for (const stackFrame of parsedStack) {
18
+ if (stackFrame.file === originalFilePath) {
19
+ if (stackFrame.column || stackFrame.lineNumber) {
20
+ const positionWithError = sourceMapConsumer.originalPositionFor({
21
+ column: stackFrame.column ?? 0,
22
+ line: stackFrame.lineNumber ?? 0,
23
+ });
24
+ const columnAndLine =
25
+ positionWithError.column && positionWithError.line
26
+ ? `${positionWithError.line}:${positionWithError.column}`
27
+ : positionWithError.line;
28
+ newStackLines.push(
29
+ ` at ${stackFrame.methodName} (${originalFilePath}:${columnAndLine})`,
30
+ );
31
+ } else {
32
+ newStackLines.push(
33
+ ` at ${stackFrame.methodName} (${originalFilePath})`,
34
+ );
35
+ }
36
+ } else {
37
+ const columnAndLine =
38
+ stackFrame.column && stackFrame.lineNumber
39
+ ? `${stackFrame.lineNumber}:${stackFrame.column}`
40
+ : stackFrame.lineNumber;
41
+ newStackLines.push(
42
+ ` at ${stackFrame.methodName} (${stackFrame.file}:${columnAndLine})`,
43
+ );
44
+ }
45
+ }
46
+ stack = newStackLines.join('\n');
47
+ }
48
+
49
+ return {
50
+ name: error.name,
51
+ message: error.message,
52
+ cause: error.cause,
53
+ stack,
54
+ };
55
+ };
@@ -1,3 +1,8 @@
1
+ /**
2
+ * An object that mimics the structure of the Error class,
3
+ * we just can't use the Error class here because server actions can't
4
+ * return classes
5
+ */
1
6
  export interface ErrorObject {
2
7
  name: string;
3
8
  stack: string | undefined;