@react-email/preview-server 4.1.3 → 4.2.1
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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +32 -32
- package/.next/build-manifest.json +14 -14
- package/.next/diagnostics/framework.json +1 -1
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +17 -8
- package/.next/routes-manifest.json +6 -2
- package/.next/server/app/_not-found/page.js +2 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/page.js +2 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/preview/[...slug]/page.js +248 -247
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/12.js +1 -0
- package/.next/server/chunks/{532.js → 37.js} +9 -9
- package/.next/server/chunks/548.js +1 -0
- package/.next/server/chunks/567.js +19 -0
- package/.next/server/chunks/592.js +6 -0
- package/.next/server/chunks/647.js +22 -0
- package/.next/server/chunks/940.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/_app.js +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js +1 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js +19 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/server/webpack-runtime.js +1 -1
- package/.next/static/{-R7XblIQtoOlIs_k4ap9z → WX9lMsWJZaaGbrYTINPW2}/_buildManifest.js +1 -1
- package/.next/static/chunks/111-fe28b5207125a701.js +1 -0
- package/.next/static/chunks/182-34462221e33239cb.js +1 -0
- package/.next/static/chunks/251-988548d3856d975a.js +1 -0
- package/.next/static/chunks/46f3b500-a39f762ecb725444.js +1 -0
- package/.next/static/chunks/637-7e851df1de391b71.js +1 -0
- package/.next/static/chunks/692-1385e32ec5c9c364.js +1 -0
- package/.next/static/chunks/713-8e6cc7e1538bbe6f.js +1 -0
- package/.next/static/chunks/app/_not-found/page-6f85a18ad9712111.js +1 -0
- package/.next/static/chunks/app/layout-efc68a40894d4923.js +1 -0
- package/.next/static/chunks/app/page-410b4634696dec1b.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-9976d01a0991f8ad.js +1 -0
- package/.next/static/chunks/{f33a14d2-13f6de3d216cf617.js → f33a14d2-de52e979a80bd763.js} +3 -3
- package/.next/static/chunks/{framework-4f208795521d076c.js → framework-b9ddb32821d5de4a.js} +1 -1
- package/.next/static/chunks/main-014ba811a7be569f.js +1 -0
- package/.next/static/chunks/main-app-0dd2885b14bde4a3.js +1 -0
- package/.next/static/chunks/pages/_app-2996112b7ea6161a.js +1 -0
- package/.next/static/chunks/pages/_error-780bd9e2fc784382.js +1 -0
- package/.next/static/chunks/{webpack-fc6f324c4c88f6e3.js → webpack-240ccd28f08d7370.js} +1 -1
- package/.next/static/css/e2f28c91a6a919eb.css +3 -0
- package/.next/trace +28 -27
- package/CHANGELOG.md +12 -0
- package/jsx-runtime/jsx-dev-runtime.js +24 -0
- package/package.json +9 -9
- package/scripts/dev.mts +1 -0
- package/src/actions/render-email-by-path.tsx +122 -15
- package/src/app/env.ts +3 -0
- package/src/app/preview/[...slug]/error-overlay.tsx +57 -0
- package/src/app/preview/[...slug]/preview.tsx +2 -2
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +4 -10
- package/src/utils/{improve-error-with-sourcemap.ts → convert-stack-with-sourcemap.ts} +6 -11
- package/src/utils/create-jsx-runtime.ts +37 -0
- package/src/utils/get-email-component.spec.ts +2 -0
- package/src/utils/get-email-component.ts +42 -24
- package/src/utils/run-bundled-code.ts +10 -5
- package/.next/server/chunks/277.js +0 -1
- package/.next/server/chunks/414.js +0 -1
- package/.next/server/chunks/550.js +0 -6
- package/.next/server/chunks/834.js +0 -15
- package/.next/server/chunks/851.js +0 -20
- package/.next/static/chunks/343-0aa226f42148dce4.js +0 -1
- package/.next/static/chunks/353-5e7d05b120b2e25f.js +0 -1
- package/.next/static/chunks/445-576b0c51a037fe4d.js +0 -1
- package/.next/static/chunks/483-54a16a125041dd23.js +0 -1
- package/.next/static/chunks/624-9d1650c8211771cd.js +0 -1
- package/.next/static/chunks/755-01456539a6894956.js +0 -1
- package/.next/static/chunks/app/_not-found/page-e9c9ee8e737ce3f8.js +0 -1
- package/.next/static/chunks/app/layout-35967d6476151e65.js +0 -1
- package/.next/static/chunks/app/page-ee81303e36f65d71.js +0 -1
- package/.next/static/chunks/app/preview/[...slug]/page-26e636443cb157e8.js +0 -1
- package/.next/static/chunks/e8809b48-6a73b3f51ba71e9c.js +0 -1
- package/.next/static/chunks/main-0000d0a5ac74fec0.js +0 -1
- package/.next/static/chunks/main-app-5ba7fd8e7a976edb.js +0 -1
- package/.next/static/chunks/pages/_app-550e3587fddcaa19.js +0 -1
- package/.next/static/chunks/pages/_error-870af0ad63b0e49e.js +0 -1
- package/.next/static/css/48dcea18d820a298.css +0 -3
- package/.next/types/app/layout.ts +0 -84
- package/src/app/preview/[...slug]/rendering-error.tsx +0 -40
- /package/.next/static/{-R7XblIQtoOlIs_k4ap9z → WX9lMsWJZaaGbrYTINPW2}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @react-email/preview-server
|
|
2
2
|
|
|
3
|
+
## 4.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c273405: pin all dependencies to avoid compatibility issues of the built preview server
|
|
8
|
+
|
|
9
|
+
## 4.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- e52818c: add custom error handling for prettier's syntax errors
|
|
14
|
+
|
|
3
15
|
## 4.1.3
|
|
4
16
|
|
|
5
17
|
### 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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-email/preview-server",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"main": "./index.mjs",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@babel/core": "7.26.10",
|
|
8
|
-
"@babel/parser": "
|
|
9
|
-
"@babel/traverse": "
|
|
8
|
+
"@babel/parser": "7.27.0",
|
|
9
|
+
"@babel/traverse": "7.27.0",
|
|
10
10
|
"@lottiefiles/dotlottie-react": "0.13.3",
|
|
11
11
|
"@radix-ui/colors": "3.0.0",
|
|
12
12
|
"@radix-ui/react-collapsible": "1.1.7",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"@types/react-dom": "19.0.4",
|
|
23
23
|
"@types/webpack": "5.28.5",
|
|
24
24
|
"autoprefixer": "10.4.21",
|
|
25
|
-
"chalk": "
|
|
25
|
+
"chalk": "4.1.2",
|
|
26
26
|
"clsx": "2.1.1",
|
|
27
|
-
"esbuild": "
|
|
27
|
+
"esbuild": "0.25.0",
|
|
28
28
|
"framer-motion": "12.7.5",
|
|
29
29
|
"json5": "2.2.3",
|
|
30
|
-
"log-symbols": "
|
|
30
|
+
"log-symbols": "4.1.0",
|
|
31
31
|
"module-punycode": "npm:punycode@2.3.1",
|
|
32
|
-
"next": "
|
|
32
|
+
"next": "15.4.1",
|
|
33
33
|
"node-html-parser": "7.0.1",
|
|
34
|
-
"ora": "
|
|
34
|
+
"ora": "5.4.1",
|
|
35
35
|
"pretty-bytes": "6.1.1",
|
|
36
36
|
"prism-react-renderer": "2.4.1",
|
|
37
37
|
"react": "19.0.0",
|
|
@@ -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.
|
|
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 {
|
|
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
|
|
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
|
|
87
|
+
const element = createElement(EmailComponent, previewProps);
|
|
88
|
+
const markupWithReferences = await renderWithReferences(element, {
|
|
68
89
|
pretty: true,
|
|
69
90
|
});
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 {
|
|
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 ? <
|
|
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 {
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
7
|
-
|
|
5
|
+
export const convertStackWithSourceMap = (
|
|
6
|
+
rawStack: string | undefined,
|
|
8
7
|
|
|
9
8
|
originalFilePath: string,
|
|
10
9
|
sourceMapToOriginalFile: RawSourceMap,
|
|
11
|
-
):
|
|
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 (
|
|
36
|
-
const parsedStack = stackTraceParser.parse(
|
|
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
|
|
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:
|
|
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:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
7
|
-
|
|
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) {
|