react-email 2.1.2 → 2.1.3-canary.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.
- package/cli/index.js +44 -31
- package/cli/index.mjs +32 -28
- package/next.config.js +1 -8
- package/package.json +7 -9
- package/src/actions/get-email-path-from-slug.ts +1 -1
- package/src/actions/get-emails-directory-metadata.ts +7 -0
- package/src/actions/render-email-by-path.tsx +5 -2
- package/src/app/preview/[...slug]/page.tsx +17 -4
- package/src/components/topbar.tsx +2 -1
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
- package/src/utils/get-email-component.spec.ts +25 -0
- package/src/utils/get-email-component.ts +71 -4
- package/src/utils/improve-error-with-sourcemap.ts +42 -13
- package/src/utils/testing/email-template.tsx +9 -0
- package/dist/cli/index.d.mts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -2363
- package/dist/cli/index.mjs +0 -1087
package/cli/index.js
CHANGED
|
@@ -340,7 +340,7 @@ var import_commander = require("commander");
|
|
|
340
340
|
// package.json
|
|
341
341
|
var package_default = {
|
|
342
342
|
name: "react-email",
|
|
343
|
-
version: "2.1.
|
|
343
|
+
version: "2.1.3-canary.0",
|
|
344
344
|
description: "A live preview of your emails right in your browser.",
|
|
345
345
|
bin: {
|
|
346
346
|
email: "./cli/index.js"
|
|
@@ -373,9 +373,8 @@ var package_default = {
|
|
|
373
373
|
"@radix-ui/react-popover": "1.0.7",
|
|
374
374
|
"@radix-ui/react-slot": "1.0.2",
|
|
375
375
|
"@radix-ui/react-toggle-group": "1.0.4",
|
|
376
|
-
"@radix-ui/react-tooltip": "1.0.
|
|
377
|
-
"@react-email/
|
|
378
|
-
"@react-email/render": "0.0.13",
|
|
376
|
+
"@radix-ui/react-tooltip": "1.0.7",
|
|
377
|
+
"@react-email/render": "0.0.14-canary.0",
|
|
379
378
|
"@swc/core": "1.3.101",
|
|
380
379
|
"@types/react": "^18.2.0",
|
|
381
380
|
"@types/react-dom": "^18.2.0",
|
|
@@ -394,10 +393,10 @@ var package_default = {
|
|
|
394
393
|
glob: "10.3.4",
|
|
395
394
|
"log-symbols": "4.1.0",
|
|
396
395
|
"mime-types": "2.1.35",
|
|
397
|
-
next: "14.1.
|
|
396
|
+
next: "14.1.4",
|
|
398
397
|
"normalize-path": "3.0.0",
|
|
399
398
|
ora: "5.4.1",
|
|
400
|
-
postcss: "8.4.
|
|
399
|
+
postcss: "8.4.38",
|
|
401
400
|
"prism-react-renderer": "2.1.0",
|
|
402
401
|
react: "^18.2.0",
|
|
403
402
|
"react-dom": "^18.2.0",
|
|
@@ -420,9 +419,8 @@ var package_default = {
|
|
|
420
419
|
"@vercel/style-guide": "5.1.0",
|
|
421
420
|
eslint: "8.50.0",
|
|
422
421
|
tsup: "7.2.0",
|
|
423
|
-
tsx: "4.
|
|
424
|
-
vitest: "1.1.3"
|
|
425
|
-
watch: "1.0.2"
|
|
422
|
+
tsx: "4.9.0",
|
|
423
|
+
vitest: "1.1.3"
|
|
426
424
|
}
|
|
427
425
|
};
|
|
428
426
|
// src/cli/commands/dev.ts
|
|
@@ -1405,7 +1403,7 @@ var import_node_fs3 = require("fs");
|
|
|
1405
1403
|
var import_mime_types = require("mime-types");
|
|
1406
1404
|
var serveStaticFile = function() {
|
|
1407
1405
|
var _ref = _async_to_generator(function(res, parsedUrl, staticDirRelativePath) {
|
|
1408
|
-
var staticBaseDir, pathname, ext, fileAbsolutePath,
|
|
1406
|
+
var staticBaseDir, pathname, ext, fileAbsolutePath, fileHandle, fileData, exception;
|
|
1409
1407
|
return _ts_generator(this, function(_state) {
|
|
1410
1408
|
switch(_state.label){
|
|
1411
1409
|
case 0:
|
|
@@ -1413,37 +1411,47 @@ var serveStaticFile = function() {
|
|
|
1413
1411
|
pathname = parsedUrl.pathname;
|
|
1414
1412
|
ext = import_node_path4.default.parse(pathname).ext;
|
|
1415
1413
|
fileAbsolutePath = import_node_path4.default.join(staticBaseDir, pathname);
|
|
1416
|
-
doesFileExist = (0, import_node_fs3.existsSync)(fileAbsolutePath);
|
|
1417
|
-
if (!!doesFileExist) return [
|
|
1418
|
-
3,
|
|
1419
|
-
1
|
|
1420
|
-
];
|
|
1421
|
-
res.statusCode = 404;
|
|
1422
|
-
res.end("File ".concat(pathname, " not found!"));
|
|
1423
|
-
return [
|
|
1424
|
-
3,
|
|
1425
|
-
4
|
|
1426
|
-
];
|
|
1427
|
-
case 1:
|
|
1428
1414
|
return [
|
|
1429
1415
|
4,
|
|
1430
|
-
import_node_fs3.promises.
|
|
1416
|
+
import_node_fs3.promises.open(fileAbsolutePath, "r")
|
|
1431
1417
|
];
|
|
1418
|
+
case 1:
|
|
1419
|
+
fileHandle = _state.sent();
|
|
1420
|
+
_state.label = 2;
|
|
1432
1421
|
case 2:
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1422
|
+
_state.trys.push([
|
|
1423
|
+
2,
|
|
1424
|
+
4,
|
|
1425
|
+
5,
|
|
1426
|
+
6
|
|
1427
|
+
]);
|
|
1437
1428
|
return [
|
|
1438
1429
|
4,
|
|
1439
|
-
import_node_fs3.promises.readFile(
|
|
1430
|
+
import_node_fs3.promises.readFile(fileHandle)
|
|
1440
1431
|
];
|
|
1441
1432
|
case 3:
|
|
1442
1433
|
fileData = _state.sent();
|
|
1443
1434
|
res.setHeader("Content-type", (0, import_mime_types.lookup)(ext) || "text/plain");
|
|
1444
1435
|
res.end(fileData);
|
|
1445
|
-
|
|
1436
|
+
return [
|
|
1437
|
+
3,
|
|
1438
|
+
6
|
|
1439
|
+
];
|
|
1446
1440
|
case 4:
|
|
1441
|
+
exception = _state.sent();
|
|
1442
|
+
console.error("Could not read file at ".concat(fileAbsolutePath, " to be served, here's the exception:"), exception);
|
|
1443
|
+
res.statusCode = 500;
|
|
1444
|
+
res.end("Could not read file to be served! Check your terminal for more information.");
|
|
1445
|
+
return [
|
|
1446
|
+
3,
|
|
1447
|
+
6
|
|
1448
|
+
];
|
|
1449
|
+
case 5:
|
|
1450
|
+
fileHandle.close();
|
|
1451
|
+
return [
|
|
1452
|
+
7
|
|
1453
|
+
];
|
|
1454
|
+
case 6:
|
|
1447
1455
|
return [
|
|
1448
1456
|
2
|
|
1449
1457
|
];
|
|
@@ -1699,6 +1707,9 @@ var isFileAnEmail = function(fullPath) {
|
|
|
1699
1707
|
".tsx",
|
|
1700
1708
|
".jsx"
|
|
1701
1709
|
].includes(ext)) return false;
|
|
1710
|
+
if (!import_node_fs5.default.existsSync(fullPath)) {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1702
1713
|
var fileContents = import_node_fs5.default.readFileSync(fullPath, "utf8");
|
|
1703
1714
|
return /\bexport\s+default\b/gm.test(fileContents);
|
|
1704
1715
|
};
|
|
@@ -1968,7 +1979,7 @@ var setNextEnvironmentVariablesForBuild = function() {
|
|
|
1968
1979
|
import_node_path8.default.normalize(emailsDirRelativePath), "PLACEHOLDER", "PLACEHOLDER")), {
|
|
1969
1980
|
NEXT_PUBLIC_IS_BUILDING: "true"
|
|
1970
1981
|
});
|
|
1971
|
-
nextConfigContents = "\nconst path = require('path');\n/** @type {import('next').NextConfig} */\nmodule.exports = {\n env: {\n ...".concat(JSON.stringify(envVariables), ",\n NEXT_PUBLIC_USER_PROJECT_LOCATION: path.resolve(process.cwd(), '../'),\n NEXT_PUBLIC_CLI_PACKAGE_LOCATION: process.cwd(),\n },\n // this is needed so that the code for building emails works properly\n webpack: (\n /** @type {import('webpack').Configuration & { externals: string[] }} */\n config,\n { isServer }\n ) => {\n if (isServer) {\n config.externals.push('esbuild');\n }\n\n return config;\n },\n typescript: {\n ignoreBuildErrors: true\n },\n eslint: {\n ignoreDuringBuilds: true\n },\n experimental: {\n webpackBuildWorker: true
|
|
1982
|
+
nextConfigContents = "\nconst path = require('path');\n/** @type {import('next').NextConfig} */\nmodule.exports = {\n env: {\n ...".concat(JSON.stringify(envVariables), ",\n NEXT_PUBLIC_USER_PROJECT_LOCATION: path.resolve(process.cwd(), '../'),\n NEXT_PUBLIC_CLI_PACKAGE_LOCATION: process.cwd(),\n },\n // this is needed so that the code for building emails works properly\n webpack: (\n /** @type {import('webpack').Configuration & { externals: string[] }} */\n config,\n { isServer }\n ) => {\n if (isServer) {\n config.externals.push('esbuild');\n }\n\n return config;\n },\n typescript: {\n ignoreBuildErrors: true\n },\n eslint: {\n ignoreDuringBuilds: true\n },\n experimental: {\n webpackBuildWorker: true\n },\n}");
|
|
1972
1983
|
return [
|
|
1973
1984
|
4,
|
|
1974
1985
|
import_node_fs7.default.promises.writeFile(import_node_path8.default.resolve(builtPreviewAppPath, "./next.config.js"), nextConfigContents, "utf8")
|
|
@@ -2019,7 +2030,7 @@ var forceSSGForEmailPreviews = function() {
|
|
|
2019
2030
|
});
|
|
2020
2031
|
return [
|
|
2021
2032
|
4,
|
|
2022
|
-
import_node_fs7.default.promises.appendFile(import_node_path8.default.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"), "\n\nexport
|
|
2033
|
+
import_node_fs7.default.promises.appendFile(import_node_path8.default.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"), "\n\nexport function generateStaticParams() { \n return Promise.resolve(\n ".concat(JSON.stringify(parameters), "\n );\n}"), "utf8")
|
|
2023
2034
|
];
|
|
2024
2035
|
case 2:
|
|
2025
2036
|
_state.sent();
|
|
@@ -2051,6 +2062,8 @@ var updatePackageJson = function() {
|
|
|
2051
2062
|
]);
|
|
2052
2063
|
packageJson.scripts.build = "next build";
|
|
2053
2064
|
packageJson.scripts.start = "next start";
|
|
2065
|
+
packageJson.name = "preview-server";
|
|
2066
|
+
delete packageJson.dependencies["@react-email/render"];
|
|
2054
2067
|
packageJson.dependencies.sharp = "0.33.2";
|
|
2055
2068
|
return [
|
|
2056
2069
|
4,
|
package/cli/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { program } from "commander";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "react-email",
|
|
16
|
-
version: "2.1.
|
|
16
|
+
version: "2.1.3-canary.0",
|
|
17
17
|
description: "A live preview of your emails right in your browser.",
|
|
18
18
|
bin: {
|
|
19
19
|
email: "./cli/index.js"
|
|
@@ -46,9 +46,8 @@ var package_default = {
|
|
|
46
46
|
"@radix-ui/react-popover": "1.0.7",
|
|
47
47
|
"@radix-ui/react-slot": "1.0.2",
|
|
48
48
|
"@radix-ui/react-toggle-group": "1.0.4",
|
|
49
|
-
"@radix-ui/react-tooltip": "1.0.
|
|
50
|
-
"@react-email/
|
|
51
|
-
"@react-email/render": "0.0.13",
|
|
49
|
+
"@radix-ui/react-tooltip": "1.0.7",
|
|
50
|
+
"@react-email/render": "0.0.14-canary.0",
|
|
52
51
|
"@swc/core": "1.3.101",
|
|
53
52
|
"@types/react": "^18.2.0",
|
|
54
53
|
"@types/react-dom": "^18.2.0",
|
|
@@ -67,10 +66,10 @@ var package_default = {
|
|
|
67
66
|
glob: "10.3.4",
|
|
68
67
|
"log-symbols": "4.1.0",
|
|
69
68
|
"mime-types": "2.1.35",
|
|
70
|
-
next: "14.1.
|
|
69
|
+
next: "14.1.4",
|
|
71
70
|
"normalize-path": "3.0.0",
|
|
72
71
|
ora: "5.4.1",
|
|
73
|
-
postcss: "8.4.
|
|
72
|
+
postcss: "8.4.38",
|
|
74
73
|
"prism-react-renderer": "2.1.0",
|
|
75
74
|
react: "^18.2.0",
|
|
76
75
|
"react-dom": "^18.2.0",
|
|
@@ -93,9 +92,8 @@ var package_default = {
|
|
|
93
92
|
"@vercel/style-guide": "5.1.0",
|
|
94
93
|
eslint: "8.50.0",
|
|
95
94
|
tsup: "7.2.0",
|
|
96
|
-
tsx: "4.
|
|
97
|
-
vitest: "1.1.3"
|
|
98
|
-
watch: "1.0.2"
|
|
95
|
+
tsx: "4.9.0",
|
|
96
|
+
vitest: "1.1.3"
|
|
99
97
|
}
|
|
100
98
|
};
|
|
101
99
|
|
|
@@ -444,25 +442,29 @@ var closeOraOnSIGNIT = (spinner) => {
|
|
|
444
442
|
|
|
445
443
|
// src/cli/utils/preview/serve-static-file.ts
|
|
446
444
|
import path4 from "path";
|
|
447
|
-
import { promises as fs3
|
|
445
|
+
import { promises as fs3 } from "fs";
|
|
448
446
|
import { lookup } from "mime-types";
|
|
449
447
|
var serveStaticFile = async (res, parsedUrl, staticDirRelativePath) => {
|
|
450
448
|
const staticBaseDir = path4.join(process.cwd(), staticDirRelativePath);
|
|
451
449
|
const pathname = parsedUrl.pathname;
|
|
452
450
|
const ext = path4.parse(pathname).ext;
|
|
453
451
|
let fileAbsolutePath = path4.join(staticBaseDir, pathname);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
res.end(`File ${pathname} not found!`);
|
|
458
|
-
} else {
|
|
459
|
-
const fileStat = await fs3.stat(fileAbsolutePath);
|
|
460
|
-
if (fileStat.isDirectory()) {
|
|
461
|
-
fileAbsolutePath += `/index${ext}`;
|
|
462
|
-
}
|
|
463
|
-
const fileData = await fs3.readFile(fileAbsolutePath);
|
|
452
|
+
const fileHandle = await fs3.open(fileAbsolutePath, "r");
|
|
453
|
+
try {
|
|
454
|
+
const fileData = await fs3.readFile(fileHandle);
|
|
464
455
|
res.setHeader("Content-type", lookup(ext) || "text/plain");
|
|
465
456
|
res.end(fileData);
|
|
457
|
+
} catch (exception) {
|
|
458
|
+
console.error(
|
|
459
|
+
`Could not read file at ${fileAbsolutePath} to be served, here's the exception:`,
|
|
460
|
+
exception
|
|
461
|
+
);
|
|
462
|
+
res.statusCode = 500;
|
|
463
|
+
res.end(
|
|
464
|
+
`Could not read file to be served! Check your terminal for more information.`
|
|
465
|
+
);
|
|
466
|
+
} finally {
|
|
467
|
+
fileHandle.close();
|
|
466
468
|
}
|
|
467
469
|
};
|
|
468
470
|
|
|
@@ -651,6 +653,9 @@ var isFileAnEmail = (fullPath) => {
|
|
|
651
653
|
const { ext } = path7.parse(fullPath);
|
|
652
654
|
if (![".js", ".tsx", ".jsx"].includes(ext))
|
|
653
655
|
return false;
|
|
656
|
+
if (!fs5.existsSync(fullPath)) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
654
659
|
const fileContents = fs5.readFileSync(fullPath, "utf8");
|
|
655
660
|
return /\bexport\s+default\b/gm.test(fileContents);
|
|
656
661
|
};
|
|
@@ -883,12 +888,7 @@ module.exports = {
|
|
|
883
888
|
ignoreDuringBuilds: true
|
|
884
889
|
},
|
|
885
890
|
experimental: {
|
|
886
|
-
webpackBuildWorker: true
|
|
887
|
-
serverComponentsExternalPackages: [
|
|
888
|
-
'@react-email/components',
|
|
889
|
-
'@react-email/render',
|
|
890
|
-
'@react-email/tailwind',
|
|
891
|
-
],
|
|
891
|
+
webpackBuildWorker: true
|
|
892
892
|
},
|
|
893
893
|
}`;
|
|
894
894
|
await fs7.promises.writeFile(
|
|
@@ -928,8 +928,10 @@ var forceSSGForEmailPreviews = async (emailsDirPath, builtPreviewAppPath) => {
|
|
|
928
928
|
path9.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"),
|
|
929
929
|
`
|
|
930
930
|
|
|
931
|
-
export
|
|
932
|
-
return
|
|
931
|
+
export function generateStaticParams() {
|
|
932
|
+
return Promise.resolve(
|
|
933
|
+
${JSON.stringify(parameters)}
|
|
934
|
+
);
|
|
933
935
|
}`,
|
|
934
936
|
"utf8"
|
|
935
937
|
);
|
|
@@ -941,6 +943,8 @@ var updatePackageJson = async (builtPreviewAppPath) => {
|
|
|
941
943
|
);
|
|
942
944
|
packageJson.scripts.build = "next build";
|
|
943
945
|
packageJson.scripts.start = "next start";
|
|
946
|
+
packageJson.name = "preview-server";
|
|
947
|
+
delete packageJson.dependencies["@react-email/render"];
|
|
944
948
|
packageJson.dependencies.sharp = "0.33.2";
|
|
945
949
|
await fs7.promises.writeFile(
|
|
946
950
|
packageJsonPath,
|
package/next.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3-canary.0",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"email": "./cli/index.js"
|
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
"@radix-ui/react-popover": "1.0.7",
|
|
26
26
|
"@radix-ui/react-slot": "1.0.2",
|
|
27
27
|
"@radix-ui/react-toggle-group": "1.0.4",
|
|
28
|
-
"@radix-ui/react-tooltip": "1.0.
|
|
29
|
-
"@react-email/
|
|
30
|
-
"@react-email/render": "0.0.13",
|
|
28
|
+
"@radix-ui/react-tooltip": "1.0.7",
|
|
29
|
+
"@react-email/render": "0.0.14-canary.0",
|
|
31
30
|
"@swc/core": "1.3.101",
|
|
32
31
|
"@types/react": "^18.2.0",
|
|
33
32
|
"@types/react-dom": "^18.2.0",
|
|
@@ -46,10 +45,10 @@
|
|
|
46
45
|
"glob": "10.3.4",
|
|
47
46
|
"log-symbols": "4.1.0",
|
|
48
47
|
"mime-types": "2.1.35",
|
|
49
|
-
"next": "14.1.
|
|
48
|
+
"next": "14.1.4",
|
|
50
49
|
"normalize-path": "3.0.0",
|
|
51
50
|
"ora": "5.4.1",
|
|
52
|
-
"postcss": "8.4.
|
|
51
|
+
"postcss": "8.4.38",
|
|
53
52
|
"prism-react-renderer": "2.1.0",
|
|
54
53
|
"react": "^18.2.0",
|
|
55
54
|
"react-dom": "^18.2.0",
|
|
@@ -72,9 +71,8 @@
|
|
|
72
71
|
"@vercel/style-guide": "5.1.0",
|
|
73
72
|
"eslint": "8.50.0",
|
|
74
73
|
"tsup": "7.2.0",
|
|
75
|
-
"tsx": "4.
|
|
76
|
-
"vitest": "1.1.3"
|
|
77
|
-
"watch": "1.0.2"
|
|
74
|
+
"tsx": "4.9.0",
|
|
75
|
+
"vitest": "1.1.3"
|
|
78
76
|
},
|
|
79
77
|
"scripts": {
|
|
80
78
|
"build": "tsup",
|
|
@@ -19,7 +19,7 @@ export const getEmailPathFromSlug = async (slug: string) => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
throw new Error(
|
|
22
|
-
`Could not find your email file based on the slug by guessing the file extension. Tried .tsx, .jsx and .js.
|
|
22
|
+
`Could not find your email file based on the slug (${slug}) by guessing the file extension. Tried .tsx, .jsx and .js.
|
|
23
23
|
|
|
24
24
|
This is most likely not an issue with the preview server. It most likely is that the email doesn't exist.`,
|
|
25
25
|
);
|
|
@@ -12,6 +12,13 @@ const isFileAnEmail = (fullPath: string): boolean => {
|
|
|
12
12
|
|
|
13
13
|
if (!['.js', '.tsx', '.jsx'].includes(ext)) return false;
|
|
14
14
|
|
|
15
|
+
// This is to avoid a possible race condition where the file doesn't exist anymore
|
|
16
|
+
// once we are checking if it is an actual email, this couuld cause issues that
|
|
17
|
+
// would be very hard to debug and find out the why of it happening.
|
|
18
|
+
if (!fs.existsSync(fullPath)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
// check with a heuristic to see if the file has at least
|
|
16
23
|
// a default export
|
|
17
24
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import { renderAsync } from '@react-email/render';
|
|
4
3
|
import { getEmailComponent } from '../utils/get-email-component';
|
|
5
4
|
import type { ErrorObject } from '../utils/types/error-object';
|
|
6
5
|
import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
|
|
@@ -26,7 +25,11 @@ export const renderEmailByPath = async (
|
|
|
26
25
|
return { error: result.error };
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
emailComponent: Email,
|
|
30
|
+
renderAsync,
|
|
31
|
+
sourceMapToOriginalFile,
|
|
32
|
+
} = result;
|
|
30
33
|
|
|
31
34
|
const previewProps = Email.PreviewProps || {};
|
|
32
35
|
const EmailComponent = Email as React.FC;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Suspense } from 'react';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { redirect } from 'next/navigation';
|
|
2
4
|
import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
|
|
3
5
|
import { getEmailsDirectoryMetadata } from '../../../actions/get-emails-directory-metadata';
|
|
4
6
|
import { renderEmailByPath } from '../../../actions/render-email-by-path';
|
|
@@ -12,7 +14,7 @@ export interface PreviewParams {
|
|
|
12
14
|
slug: string[];
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
const Page = async ({ params }: { params: PreviewParams }) => {
|
|
16
18
|
// will come in here as segments of a relative path to the email
|
|
17
19
|
// ex: ['authentication', 'verify-password.tsx']
|
|
18
20
|
const slug = params.slug.join('/');
|
|
@@ -28,7 +30,16 @@ This is most likely not an issue with the preview server. Maybe there was a typo
|
|
|
28
30
|
);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
let emailPath: string;
|
|
34
|
+
try {
|
|
35
|
+
emailPath = await getEmailPathFromSlug(slug);
|
|
36
|
+
} catch (exception) {
|
|
37
|
+
if (exception instanceof Error) {
|
|
38
|
+
console.warn(exception.message);
|
|
39
|
+
redirect('/');
|
|
40
|
+
}
|
|
41
|
+
throw exception;
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
const emailRenderingResult = await renderEmailByPath(emailPath);
|
|
34
45
|
|
|
@@ -53,8 +64,10 @@ This is most likely not an issue with the preview server. Maybe there was a typo
|
|
|
53
64
|
/>
|
|
54
65
|
</Suspense>
|
|
55
66
|
);
|
|
56
|
-
}
|
|
67
|
+
};
|
|
57
68
|
|
|
58
69
|
export function generateMetadata({ params }: { params: PreviewParams }) {
|
|
59
|
-
return { title: `${params.slug.join('/')} — React Email` };
|
|
70
|
+
return { title: `${path.basename(params.slug.join('/'))} — React Email` };
|
|
60
71
|
}
|
|
72
|
+
|
|
73
|
+
export default Page;
|
|
@@ -11,6 +11,7 @@ import { IconPhone } from './icons/icon-phone';
|
|
|
11
11
|
import { IconSource } from './icons/icon-source';
|
|
12
12
|
import { Send } from './send';
|
|
13
13
|
import { Tooltip } from './tooltip';
|
|
14
|
+
import { pathSeparator } from '../utils/emails-directory-absolute-path';
|
|
14
15
|
|
|
15
16
|
interface TopbarProps {
|
|
16
17
|
currentEmailOpenSlug: string;
|
|
@@ -49,7 +50,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
|
|
|
49
50
|
|
|
50
51
|
<div className="items-center overflow-hidden hidden lg:flex text-center absolute left-1/2 transform -translate-x-1/2 top-1/2 -translate-y-1/2">
|
|
51
52
|
<Heading as="h2" className="truncate" size="2" weight="medium">
|
|
52
|
-
{currentEmailOpenSlug}
|
|
53
|
+
{currentEmailOpenSlug.split(pathSeparator).pop()}
|
|
53
54
|
</Heading>
|
|
54
55
|
</div>
|
|
55
56
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
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"><div>Hello <!-- -->Gabriel<!-- -->!</div>"`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { getEmailComponent } from './get-email-component';
|
|
4
|
+
|
|
5
|
+
test('getEmailComponent() with a demo email template', async () => {
|
|
6
|
+
const result = await getEmailComponent(
|
|
7
|
+
path.resolve(__dirname, './testing/email-template.tsx'),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
if ('error' in result) {
|
|
11
|
+
console.log(result.error);
|
|
12
|
+
expect('error' in result).toBe(false);
|
|
13
|
+
} else {
|
|
14
|
+
expect(result.emailComponent).toBeTruthy();
|
|
15
|
+
expect(result.sourceMapToOriginalFile).toBeTruthy();
|
|
16
|
+
|
|
17
|
+
const emailHtml = await result.renderAsync(
|
|
18
|
+
React.createElement(
|
|
19
|
+
result.emailComponent,
|
|
20
|
+
result.emailComponent.PreviewProps,
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
expect(emailHtml).toMatchSnapshot();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import vm from 'node:vm';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
4
5
|
import { type RawSourceMap } from 'source-map-js';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type OutputFile,
|
|
8
|
+
build,
|
|
9
|
+
type ResolveOptions,
|
|
10
|
+
type BuildFailure,
|
|
11
|
+
type Loader,
|
|
12
|
+
} from 'esbuild';
|
|
13
|
+
import type { renderAsync } from '@react-email/render';
|
|
6
14
|
import type { EmailTemplate as EmailComponent } from './types/email-template';
|
|
7
15
|
import type { ErrorObject } from './types/error-object';
|
|
8
16
|
import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
|
|
@@ -14,6 +22,8 @@ export const getEmailComponent = async (
|
|
|
14
22
|
| {
|
|
15
23
|
emailComponent: EmailComponent;
|
|
16
24
|
|
|
25
|
+
renderAsync: typeof renderAsync;
|
|
26
|
+
|
|
17
27
|
sourceMapToOriginalFile: RawSourceMap;
|
|
18
28
|
}
|
|
19
29
|
| { error: ErrorObject }
|
|
@@ -23,8 +33,49 @@ export const getEmailComponent = async (
|
|
|
23
33
|
const buildData = await build({
|
|
24
34
|
bundle: true,
|
|
25
35
|
entryPoints: [emailPath],
|
|
36
|
+
plugins: [
|
|
37
|
+
{
|
|
38
|
+
name: 'add-export-for-render-async',
|
|
39
|
+
setup(b) {
|
|
40
|
+
b.onLoad(
|
|
41
|
+
{ filter: new RegExp(path.basename(emailPath)) },
|
|
42
|
+
async () => ({
|
|
43
|
+
contents: `${await fs.readFile(emailPath, 'utf8')};
|
|
44
|
+
export { renderAsync } from 'react-email-module-that-will-export-render'
|
|
45
|
+
`,
|
|
46
|
+
loader: path.extname(emailPath).slice(1) as Loader,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
b.onResolve(
|
|
51
|
+
{ filter: /^react-email-module-that-will-export-render$/ },
|
|
52
|
+
async (args) => {
|
|
53
|
+
const options: ResolveOptions = {
|
|
54
|
+
kind: 'import-statement',
|
|
55
|
+
importer: args.importer,
|
|
56
|
+
resolveDir: args.resolveDir,
|
|
57
|
+
namespace: args.namespace,
|
|
58
|
+
};
|
|
59
|
+
let result = await b.resolve('@react-email/render', options);
|
|
60
|
+
if (result.errors.length === 0) {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If @react-email/render does not exist, resolve to @react-email/components
|
|
65
|
+
result = await b.resolve('@react-email/components', options);
|
|
66
|
+
if (result.errors.length > 0) {
|
|
67
|
+
result.errors[0]!.text =
|
|
68
|
+
"Failed trying to import `renderAsync` from either `@react-email/render` or `@react-email/components` to be able to render your email template.\n Maybe you don't have either of them installed?";
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
26
76
|
platform: 'node',
|
|
27
77
|
write: false,
|
|
78
|
+
|
|
28
79
|
format: 'cjs',
|
|
29
80
|
jsx: 'automatic',
|
|
30
81
|
logLevel: 'silent',
|
|
@@ -62,8 +113,15 @@ export const getEmailComponent = async (
|
|
|
62
113
|
TextEncoderStream,
|
|
63
114
|
ReadableStream,
|
|
64
115
|
URL,
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
URLSearchParams,
|
|
117
|
+
Headers,
|
|
118
|
+
module: {
|
|
119
|
+
exports: {
|
|
120
|
+
default: undefined as unknown,
|
|
121
|
+
renderAsync: undefined as unknown,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
__filename: emailPath,
|
|
67
125
|
__dirname: path.dirname(emailPath),
|
|
68
126
|
require: (module: string) => {
|
|
69
127
|
if (module in staticNodeModulesForVM) {
|
|
@@ -71,7 +129,7 @@ export const getEmailComponent = async (
|
|
|
71
129
|
return staticNodeModulesForVM[module];
|
|
72
130
|
}
|
|
73
131
|
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
|
|
75
133
|
return require(`${module}`) as unknown;
|
|
76
134
|
// this stupid string templating was necessary to not have
|
|
77
135
|
// webpack warnings like:
|
|
@@ -85,11 +143,18 @@ export const getEmailComponent = async (
|
|
|
85
143
|
process,
|
|
86
144
|
};
|
|
87
145
|
const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
|
|
146
|
+
// because it will have a path like <tsconfigLocation>/stdout/email.js.map
|
|
147
|
+
sourceMapToEmail.sourceRoot = path.resolve(sourceMapFile.path, '../..');
|
|
148
|
+
sourceMapToEmail.sources = sourceMapToEmail.sources.map((source) =>
|
|
149
|
+
path.resolve(sourceMapFile.path, '..', source),
|
|
150
|
+
);
|
|
88
151
|
try {
|
|
89
152
|
vm.runInNewContext(builtEmailCode, fakeContext, { filename: emailPath });
|
|
90
153
|
} catch (exception) {
|
|
91
154
|
const error = exception as Error;
|
|
92
155
|
|
|
156
|
+
error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
|
|
157
|
+
|
|
93
158
|
return {
|
|
94
159
|
error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
|
|
95
160
|
};
|
|
@@ -109,6 +174,8 @@ export const getEmailComponent = async (
|
|
|
109
174
|
|
|
110
175
|
return {
|
|
111
176
|
emailComponent: fakeContext.module.exports.default as EmailComponent,
|
|
177
|
+
renderAsync: fakeContext.module.exports.renderAsync as typeof renderAsync,
|
|
178
|
+
|
|
112
179
|
sourceMapToOriginalFile: sourceMapToEmail,
|
|
113
180
|
};
|
|
114
181
|
};
|