@react-email/preview-server 4.1.3 → 4.2.0

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 (48) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +5 -5
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_not-found/page.js +1 -1
  8. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  9. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/page.js +1 -1
  11. package/.next/server/app/page.js.nft.json +1 -1
  12. package/.next/server/app/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/preview/[...slug]/page.js +19 -18
  14. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  15. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  16. package/.next/server/chunks/530.js +1 -0
  17. package/.next/server/chunks/715.js +1 -0
  18. package/.next/server/pages/500.html +1 -1
  19. package/.next/server/server-reference-manifest.js +1 -1
  20. package/.next/server/server-reference-manifest.json +1 -1
  21. package/.next/static/chunks/483-ea982f13ef64ac58.js +1 -0
  22. package/.next/static/chunks/app/layout-ba38c60129649bc0.js +1 -0
  23. package/.next/static/chunks/app/preview/[...slug]/page-ef840d8f38306c17.js +1 -0
  24. package/.next/static/css/e2f28c91a6a919eb.css +3 -0
  25. package/.next/trace +27 -27
  26. package/CHANGELOG.md +6 -0
  27. package/jsx-runtime/jsx-dev-runtime.js +24 -0
  28. package/package.json +2 -2
  29. package/scripts/dev.mts +1 -0
  30. package/src/actions/render-email-by-path.tsx +122 -15
  31. package/src/app/env.ts +3 -0
  32. package/src/app/preview/[...slug]/error-overlay.tsx +57 -0
  33. package/src/app/preview/[...slug]/preview.tsx +2 -2
  34. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +4 -10
  35. package/src/utils/{improve-error-with-sourcemap.ts → convert-stack-with-sourcemap.ts} +6 -11
  36. package/src/utils/create-jsx-runtime.ts +37 -0
  37. package/src/utils/get-email-component.spec.ts +2 -0
  38. package/src/utils/get-email-component.ts +42 -24
  39. package/src/utils/run-bundled-code.ts +10 -5
  40. package/.next/server/chunks/414.js +0 -1
  41. package/.next/static/chunks/483-54a16a125041dd23.js +0 -1
  42. package/.next/static/chunks/app/layout-35967d6476151e65.js +0 -1
  43. package/.next/static/chunks/app/preview/[...slug]/page-26e636443cb157e8.js +0 -1
  44. package/.next/static/css/48dcea18d820a298.css +0 -3
  45. package/.next/types/app/layout.ts +0 -84
  46. package/src/app/preview/[...slug]/rendering-error.tsx +0 -40
  47. /package/.next/static/{-R7XblIQtoOlIs_k4ap9z → ZE6fBFiuBPHv_MdtfG4Qn}/_buildManifest.js +0 -0
  48. /package/.next/static/{-R7XblIQtoOlIs_k4ap9z → ZE6fBFiuBPHv_MdtfG4Qn}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @react-email/preview-server
2
2
 
3
+ ## 4.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e52818c: add custom error handling for prettier's syntax errors
8
+
3
9
  ## 4.1.3
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,24 @@
1
+ // This hack is necessary because React forces the use of the non-dev JSX runtime
2
+ // when NODE_ENV is set to 'production', which would break the data-source references
3
+ // we need for stack traces in the preview server.
4
+ const ReactJSXDevRuntime = require('react/jsx-dev-runtime');
5
+
6
+ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
7
+ const newProps = { ...props };
8
+
9
+ if (source && shouldIncludeSourceReference) {
10
+ newProps['data-source-file'] = source.fileName;
11
+ newProps['data-source-line'] = source.lineNumber;
12
+ }
13
+
14
+ return ReactJSXDevRuntime.jsxDEV(
15
+ type,
16
+ newProps,
17
+ key,
18
+ isStaticChildren,
19
+ source,
20
+ self,
21
+ );
22
+ }
23
+
24
+ export const Fragment = ReactJSXDevRuntime.Fragment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-email/preview-server",
3
- "version": "4.1.3",
3
+ "version": "4.2.0",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "main": "./index.mjs",
6
6
  "dependencies": {
@@ -61,7 +61,7 @@
61
61
  "postcss": "8.5.3",
62
62
  "tailwindcss": "3.4.0",
63
63
  "typescript": "5.8.3",
64
- "@react-email/components": "0.3.0"
64
+ "@react-email/components": "0.3.1"
65
65
  },
66
66
  "license": "MIT",
67
67
  "repository": {
package/scripts/dev.mts CHANGED
@@ -16,6 +16,7 @@ await fs.writeFile(
16
16
  `EMAILS_DIR_RELATIVE_PATH=./emails
17
17
  EMAILS_DIR_ABSOLUTE_PATH=${emailsDirectoryPath}
18
18
  USER_PROJECT_LOCATION=${previewServerRoot}
19
+ PREVIEW_SERVER_LOCATION=${previewServerRoot}
19
20
  NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true`,
20
21
  'utf8',
21
22
  );
@@ -4,14 +4,25 @@ import path from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import logSymbols from 'log-symbols';
6
6
  import ora, { type Ora } from 'ora';
7
- import { isBuilding, isPreviewDevelopment } from '../app/env';
7
+ import {
8
+ isBuilding,
9
+ isPreviewDevelopment,
10
+ previewServerLocation,
11
+ userProjectLocation,
12
+ } from '../app/env';
13
+ import { convertStackWithSourceMap } from '../utils/convert-stack-with-sourcemap';
14
+ import { createJsxRuntime } from '../utils/create-jsx-runtime';
8
15
  import { getEmailComponent } from '../utils/get-email-component';
9
- import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
10
16
  import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';
11
17
  import type { ErrorObject } from '../utils/types/error-object';
12
18
 
13
19
  export interface RenderedEmailMetadata {
14
20
  markup: string;
21
+ /**
22
+ * HTML markup with `data-source-file` and `data-source-line` attributes pointing to the original
23
+ * .jsx/.tsx files corresponding to the rendered tag
24
+ */
25
+ markupWithReferences?: string;
15
26
  plainText: string;
16
27
  reactMarkup: string;
17
28
  }
@@ -44,7 +55,15 @@ export const renderEmailByPath = async (
44
55
  registerSpinnerAutostopping(spinner);
45
56
  }
46
57
 
47
- const componentResult = await getEmailComponent(emailPath);
58
+ const originalJsxRuntimePath = path.resolve(
59
+ previewServerLocation,
60
+ 'jsx-runtime',
61
+ );
62
+ const jsxRuntimePath = await createJsxRuntime(
63
+ userProjectLocation,
64
+ originalJsxRuntimePath,
65
+ );
66
+ const componentResult = await getEmailComponent(emailPath, jsxRuntimePath);
48
67
 
49
68
  if ('error' in componentResult) {
50
69
  spinner?.stopAndPersist({
@@ -58,21 +77,23 @@ export const renderEmailByPath = async (
58
77
  emailComponent: Email,
59
78
  createElement,
60
79
  render,
80
+ renderWithReferences,
61
81
  sourceMapToOriginalFile,
62
82
  } = componentResult;
63
83
 
64
84
  const previewProps = Email.PreviewProps || {};
65
85
  const EmailComponent = Email as React.FC;
66
86
  try {
67
- const markup = await render(createElement(EmailComponent, previewProps), {
87
+ const element = createElement(EmailComponent, previewProps);
88
+ const markupWithReferences = await renderWithReferences(element, {
68
89
  pretty: true,
69
90
  });
70
- const plainText = await render(
71
- createElement(EmailComponent, previewProps),
72
- {
73
- plainText: true,
74
- },
75
- );
91
+ const markup = await render(element, {
92
+ pretty: true,
93
+ });
94
+ const plainText = await render(element, {
95
+ plainText: true,
96
+ });
76
97
 
77
98
  const reactMarkup = await fs.promises.readFile(emailPath, 'utf-8');
78
99
 
@@ -90,11 +111,12 @@ export const renderEmailByPath = async (
90
111
  text: `Successfully rendered ${emailFilename} in ${timeForConsole}`,
91
112
  });
92
113
 
93
- const renderingResult = {
114
+ const renderingResult: RenderedEmailMetadata = {
94
115
  // This ensures that no null byte character ends up in the rendered
95
116
  // markup making users suspect of any issues. These null byte characters
96
117
  // only seem to happen with React 18, as it has no similar incident with React 19.
97
118
  markup: markup.replaceAll('\0', ''),
119
+ markupWithReferences: markupWithReferences.replaceAll('\0', ''),
98
120
  plainText,
99
121
  reactMarkup,
100
122
  };
@@ -110,12 +132,97 @@ export const renderEmailByPath = async (
110
132
  text: `Failed while rendering ${emailFilename}`,
111
133
  });
112
134
 
113
- return {
114
- error: improveErrorWithSourceMap(
115
- error,
135
+ if (exception instanceof SyntaxError) {
136
+ interface SpanPosition {
137
+ file: {
138
+ content: string;
139
+ };
140
+ offset: number;
141
+ line: number;
142
+ col: number;
143
+ }
144
+ // means the email's HTML was invalid and prettier threw this error
145
+ // TODO: always throw when the HTML is invalid during `render`
146
+ const cause = exception.cause as {
147
+ msg: string;
148
+ span: {
149
+ start: SpanPosition;
150
+ end: SpanPosition;
151
+ };
152
+ };
153
+
154
+ const sourceFileAttributeMatches = cause.span.start.file.content.matchAll(
155
+ /data-source-file="(?<file>[^"]*)"/g,
156
+ );
157
+ let closestSourceFileAttribute: RegExpExecArray | undefined;
158
+ for (const sourceFileAttributeMatch of sourceFileAttributeMatches) {
159
+ if (closestSourceFileAttribute === undefined) {
160
+ closestSourceFileAttribute = sourceFileAttributeMatch;
161
+ }
162
+ if (
163
+ Math.abs(sourceFileAttributeMatch.index - cause.span.start.offset) <
164
+ Math.abs(closestSourceFileAttribute.index - cause.span.start.offset)
165
+ ) {
166
+ closestSourceFileAttribute = sourceFileAttributeMatch;
167
+ }
168
+ }
169
+
170
+ const findClosestAttributeValue = (
171
+ attributeName: string,
172
+ ): string | undefined => {
173
+ const attributeMatches = cause.span.start.file.content.matchAll(
174
+ new RegExp(`${attributeName}="(?<value>[^"]*)"`, 'g'),
175
+ );
176
+ let closestAttribute: RegExpExecArray | undefined;
177
+ for (const attributeMatch of attributeMatches) {
178
+ if (closestAttribute === undefined) {
179
+ closestAttribute = attributeMatch;
180
+ }
181
+ if (
182
+ Math.abs(attributeMatch.index - cause.span.start.offset) <
183
+ Math.abs(closestAttribute.index - cause.span.start.offset)
184
+ ) {
185
+ closestAttribute = attributeMatch;
186
+ }
187
+ }
188
+ return closestAttribute?.groups?.value;
189
+ };
190
+
191
+ let stack = convertStackWithSourceMap(
192
+ error.stack,
116
193
  emailPath,
117
194
  sourceMapToOriginalFile,
118
- ),
195
+ );
196
+
197
+ const sourceFile = findClosestAttributeValue('data-source-file');
198
+ const sourceLine = findClosestAttributeValue('data-source-line');
199
+ if (sourceFile && sourceLine) {
200
+ stack = ` at ${sourceFile}:${sourceLine}\n${stack}`;
201
+ }
202
+
203
+ return {
204
+ error: {
205
+ name: exception.name,
206
+ message: cause.msg,
207
+ stack,
208
+ cause: error.cause ? JSON.parse(JSON.stringify(cause)) : undefined,
209
+ },
210
+ };
211
+ }
212
+
213
+ return {
214
+ error: {
215
+ name: error.name,
216
+ message: error.message,
217
+ stack: convertStackWithSourceMap(
218
+ error.stack,
219
+ emailPath,
220
+ sourceMapToOriginalFile,
221
+ ),
222
+ cause: error.cause
223
+ ? JSON.parse(JSON.stringify(error.cause))
224
+ : undefined,
225
+ },
119
226
  };
120
227
  }
121
228
  };
package/src/app/env.ts CHANGED
@@ -5,6 +5,9 @@ export const emailsDirRelativePath = process.env.EMAILS_DIR_RELATIVE_PATH!;
5
5
  /** ONLY ACCESSIBLE ON THE SERVER */
6
6
  export const userProjectLocation = process.env.USER_PROJECT_LOCATION!;
7
7
 
8
+ /** ONLY ACCESSIBLE ON THE SERVER */
9
+ export const previewServerLocation = process.env.PREVIEW_SERVER_LOCATION!;
10
+
8
11
  /** ONLY ACCESSIBLE ON THE SERVER */
9
12
  export const emailsDirectoryAbsolutePath =
10
13
  process.env.EMAILS_DIR_ABSOLUTE_PATH!;
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+ import type { ErrorObject } from '../../../utils/types/error-object';
3
+
4
+ interface ErrorOverlayProps {
5
+ error: ErrorObject;
6
+ }
7
+
8
+ const Message = ({ children: content }: { children: string }) => {
9
+ const match = content.match(
10
+ /(Unexpected closing tag "[^"]+". It may happen when the tag has already been closed by another tag). (For more info see) (.+)/,
11
+ );
12
+ if (match) {
13
+ const [_, errorMessage, moreInfo, link] = match;
14
+ return (
15
+ <>
16
+ {errorMessage}.
17
+ <p className="text-lg">
18
+ {moreInfo}{' '}
19
+ <a className="underline" rel="noreferrer" target="_blank" href={link}>
20
+ {link}
21
+ </a>
22
+ </p>
23
+ </>
24
+ );
25
+ }
26
+ return content;
27
+ };
28
+
29
+ export const ErrorOverlay = ({ error }: ErrorOverlayProps) => {
30
+ return (
31
+ <>
32
+ <div className="absolute inset-0 z-50 bg-black/80" />
33
+ <div
34
+ className="
35
+ min-h-[50vh] w-full max-w-lg sm:rounded-lg md:max-w-[568px] lg:max-w-[920px]
36
+ absolute left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]
37
+ rounded-t-sm overflow-hidden bg-white text-black shadow-lg duration-200
38
+ flex flex-col selection:!text-black
39
+ "
40
+ >
41
+ <div className="bg-red-500 h-3" />
42
+ <div className="flex flex-grow p-6 min-w-0 max-w-full flex-col space-y-1.5">
43
+ <div className="flex-shrink pb-2 text-xl tracking-tight">
44
+ <b>{error.name}</b>: <Message>{error.message}</Message>
45
+ </div>
46
+ {error.stack ? (
47
+ <div className="flex-grow scroll-px-4 overflow-x-auto rounded-lg bg-black p-2 text-gray-100">
48
+ <pre className="w-full min-w-0 font-mono leading-6 selection:!text-cyan-12 text-xs">
49
+ {error.stack}
50
+ </pre>
51
+ </div>
52
+ ) : undefined}
53
+ </div>
54
+ </div>
55
+ </>
56
+ );
57
+ };
@@ -19,7 +19,7 @@ import { ViewSizeControls } from '../../../components/topbar/view-size-controls'
19
19
  import { PreviewContext } from '../../../contexts/preview';
20
20
  import { useClampedState } from '../../../hooks/use-clamped-state';
21
21
  import { cn } from '../../../utils';
22
- import { RenderingError } from './rendering-error';
22
+ import { ErrorOverlay } from './error-overlay';
23
23
 
24
24
  interface PreviewProps extends React.ComponentProps<'div'> {
25
25
  emailTitle: string;
@@ -136,7 +136,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
136
136
  };
137
137
  }}
138
138
  >
139
- {hasErrors ? <RenderingError error={renderingResult.error} /> : null}
139
+ {hasErrors ? <ErrorOverlay error={renderingResult.error} /> : null}
140
140
 
141
141
  {hasRenderingMetadata ? (
142
142
  <>
@@ -5,7 +5,7 @@ import * as esbuild from 'esbuild';
5
5
  import type { RawSourceMap } from 'source-map-js';
6
6
  import type { Config as TailwindOriginalConfig } from 'tailwindcss';
7
7
  import type { AST } from '../../../actions/email-validation/check-compatibility';
8
- import { improveErrorWithSourceMap } from '../../improve-error-with-sourcemap';
8
+ import { convertStackWithSourceMap } from '../../convert-stack-with-sourcemap';
9
9
  import { isErr } from '../../result';
10
10
  import { runBundledCode } from '../../run-bundled-code';
11
11
 
@@ -101,15 +101,9 @@ export { reactEmailTailwindConfigInternal };`,
101
101
  sourceMap.sources = sourceMap.sources.map((source) =>
102
102
  path.resolve(sourceMapFile.path, '..', source),
103
103
  );
104
- const errorObject = improveErrorWithSourceMap(
105
- configModule.error as Error,
106
- filepath,
107
- sourceMap,
108
- );
109
- const error = new Error();
110
- error.name = errorObject.name;
111
- error.message = errorObject.message;
112
- error.stack = errorObject.stack;
104
+
105
+ const error = configModule.error as Error;
106
+ error.stack = convertStackWithSourceMap(error.stack, filepath, sourceMap);
113
107
  throw error;
114
108
  }
115
109
 
@@ -1,14 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { type RawSourceMap, SourceMapConsumer } from 'source-map-js';
3
3
  import * as stackTraceParser from 'stacktrace-parser';
4
- import type { ErrorObject } from './types/error-object';
5
4
 
6
- export const improveErrorWithSourceMap = (
7
- error: Error,
5
+ export const convertStackWithSourceMap = (
6
+ rawStack: string | undefined,
8
7
 
9
8
  originalFilePath: string,
10
9
  sourceMapToOriginalFile: RawSourceMap,
11
- ): ErrorObject => {
10
+ ): string | undefined => {
12
11
  let stack: string | undefined;
13
12
 
14
13
  const sourceRoot =
@@ -32,8 +31,8 @@ export const improveErrorWithSourceMap = (
32
31
  })`;
33
32
  };
34
33
 
35
- if (typeof error.stack !== 'undefined') {
36
- const parsedStack = stackTraceParser.parse(error.stack);
34
+ if (rawStack) {
35
+ const parsedStack = stackTraceParser.parse(rawStack);
37
36
  const sourceMapConsumer = new SourceMapConsumer(sourceMapToOriginalFile);
38
37
  const newStackLines = [] as string[];
39
38
  for (const stackFrame of parsedStack) {
@@ -77,9 +76,5 @@ export const improveErrorWithSourceMap = (
77
76
  stack = newStackLines.join('\n');
78
77
  }
79
78
 
80
- return {
81
- name: error.name,
82
- message: error.message,
83
- stack,
84
- };
79
+ return stack;
85
80
  };
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import esbuild from 'esbuild';
4
+
5
+ /**
6
+ * Bundles the JSX runtime with the specified {@link cwd}. This is needed because the JSX runtime
7
+ * imports React's which is forcefully the production one if the `NODE_ENV` is set to `production`,
8
+ * even though we want to use the development one.
9
+ *
10
+ * It bundles into `/node_modules/.react-email-jsx-runtime` with the root being the {@link cwd}.
11
+ */
12
+ export const createJsxRuntime = async (
13
+ cwd: string,
14
+ originalJsxRuntimePath: string,
15
+ ) => {
16
+ const jsxRuntimePath = path.join(
17
+ cwd,
18
+ 'node_modules',
19
+ '.react-email-jsx-runtime',
20
+ );
21
+ await esbuild.build({
22
+ bundle: true,
23
+ outfile: path.join(jsxRuntimePath, 'jsx-dev-runtime.js'),
24
+ format: 'cjs',
25
+ logLevel: 'silent',
26
+ stdin: {
27
+ resolveDir: cwd,
28
+ sourcefile: 'jsx-dev-runtime.js',
29
+ loader: 'js',
30
+ contents: await fs.promises.readFile(
31
+ path.join(originalJsxRuntimePath, 'jsx-dev-runtime.js'),
32
+ ),
33
+ },
34
+ });
35
+
36
+ return jsxRuntimePath;
37
+ };
@@ -6,6 +6,7 @@ describe('getEmailComponent()', () => {
6
6
  test('Request', async () => {
7
7
  const result = await getEmailComponent(
8
8
  path.resolve(__dirname, './testing/request-response-email.tsx'),
9
+ path.resolve(__dirname, '../../jsx-runtime'),
9
10
  );
10
11
  if ('error' in result) {
11
12
  console.log(result.error);
@@ -20,6 +21,7 @@ describe('getEmailComponent()', () => {
20
21
  __dirname,
21
22
  '../../../../apps/demo/emails/notifications/vercel-invite-user.tsx',
22
23
  ),
24
+ path.resolve(__dirname, '../../jsx-runtime'),
23
25
  );
24
26
 
25
27
  if ('error' in result) {
@@ -4,10 +4,10 @@ import { type BuildFailure, build, type OutputFile } from 'esbuild';
4
4
  import type React from 'react';
5
5
  import type { RawSourceMap } from 'source-map-js';
6
6
  import { z } from 'zod';
7
+ import { convertStackWithSourceMap } from './convert-stack-with-sourcemap';
7
8
  import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
8
- import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
9
9
  import { isErr } from './result';
10
- import { runBundledCode } from './run-bundled-code';
10
+ import { createContext, 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
 
@@ -19,12 +19,18 @@ const EmailComponentModule = z.object({
19
19
 
20
20
  export const getEmailComponent = async (
21
21
  emailPath: string,
22
+ jsxRuntimePath: string,
22
23
  ): Promise<
23
24
  | {
24
25
  emailComponent: EmailComponent;
25
26
 
26
27
  createElement: typeof React.createElement;
27
28
 
29
+ /**
30
+ * Renders the HTML with `data-source-file`/`data-source-line` attributes that should only be
31
+ * used internally in the preview server and never shown to the user.
32
+ */
33
+ renderWithReferences: typeof render;
28
34
  render: typeof render;
29
35
 
30
36
  sourceMapToOriginalFile: RawSourceMap;
@@ -40,6 +46,9 @@ export const getEmailComponent = async (
40
46
  platform: 'node',
41
47
  write: false,
42
48
 
49
+ jsxDev: true,
50
+ jsxImportSource: jsxRuntimePath,
51
+
43
52
  format: 'cjs',
44
53
  jsx: 'automatic',
45
54
  logLevel: 'silent',
@@ -74,7 +83,9 @@ export const getEmailComponent = async (
74
83
  path.resolve(sourceMapFile.path, '..', source),
75
84
  );
76
85
 
77
- const runningResult = runBundledCode(builtEmailCode, emailPath);
86
+ const context = createContext(emailPath);
87
+ context.shouldIncludeSourceReference = false;
88
+ const runningResult = runBundledCode(builtEmailCode, emailPath, context);
78
89
 
79
90
  if (isErr(runningResult)) {
80
91
  const { error } = runningResult;
@@ -82,7 +93,16 @@ export const getEmailComponent = async (
82
93
  error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
83
94
 
84
95
  return {
85
- error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
96
+ error: {
97
+ name: error.name,
98
+ message: error.message,
99
+ stack: convertStackWithSourceMap(
100
+ error.stack,
101
+ emailPath,
102
+ sourceMapToEmail,
103
+ ),
104
+ cause: error.cause,
105
+ },
86
106
  };
87
107
  }
88
108
 
@@ -93,31 +113,23 @@ export const getEmailComponent = async (
93
113
 
94
114
  if (parseResult.error) {
95
115
  return {
96
- error: improveErrorWithSourceMap(
97
- new Error(
98
- `The email component at ${emailPath} does not contain the expected exports`,
99
- {
100
- cause: parseResult.error,
101
- },
102
- ),
103
- emailPath,
104
- sourceMapToEmail,
105
- ),
116
+ error: {
117
+ name: 'Error',
118
+ message: `The email component at ${emailPath} does not contain the expected exports`,
119
+ stack: new Error().stack,
120
+ cause: parseResult.error,
121
+ },
106
122
  };
107
123
  }
108
124
 
109
125
  if (typeof parseResult.data.default !== 'function') {
110
126
  return {
111
- error: improveErrorWithSourceMap(
112
- new Error(
113
- `The email component at ${emailPath} does not contain a default exported function`,
114
- {
115
- cause: parseResult.error,
116
- },
117
- ),
118
- emailPath,
119
- sourceMapToEmail,
120
- ),
127
+ error: {
128
+ name: 'Error',
129
+ message: `The email component at ${emailPath} does not contain a default exported function`,
130
+ stack: new Error().stack,
131
+ cause: parseResult.error,
132
+ },
121
133
  };
122
134
  }
123
135
 
@@ -125,6 +137,12 @@ export const getEmailComponent = async (
125
137
 
126
138
  return {
127
139
  emailComponent: componentModule.default as EmailComponent,
140
+ renderWithReferences: (async (...args) => {
141
+ context.shouldIncludeSourceReference = true;
142
+ const renderingResult = await componentModule.render(...args);
143
+ context.shouldIncludeSourceReference = false;
144
+ return renderingResult;
145
+ }) as typeof render,
128
146
  render: componentModule.render as typeof render,
129
147
  createElement:
130
148
  componentModule.reactEmailCreateReactElement as typeof React.createElement,
@@ -3,11 +3,8 @@ import vm from 'node:vm';
3
3
  import { err, ok, type Result } from './result';
4
4
  import { staticNodeModulesForVM } from './static-node-modules-for-vm';
5
5
 
6
- export const runBundledCode = (
7
- code: string,
8
- filename: string,
9
- ): Result<unknown, unknown> => {
10
- const fakeContext = {
6
+ export const createContext = (filename: string): vm.Context => {
7
+ return {
11
8
  ...global,
12
9
  console,
13
10
  Buffer,
@@ -18,6 +15,8 @@ export const runBundledCode = (
18
15
  Request,
19
16
  Response,
20
17
  TextDecoderStream,
18
+ SyntaxError,
19
+ Error,
21
20
  TextEncoder,
22
21
  TextEncoderStream,
23
22
  ReadableStream,
@@ -53,7 +52,13 @@ export const runBundledCode = (
53
52
  },
54
53
  process,
55
54
  };
55
+ };
56
56
 
57
+ export const runBundledCode = (
58
+ code: string,
59
+ filename: string,
60
+ fakeContext: vm.Context = createContext(filename),
61
+ ): Result<unknown, unknown> => {
57
62
  try {
58
63
  vm.runInNewContext(code, fakeContext, { filename });
59
64
  } catch (exception) {