react-email 1.9.0 → 1.9.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/dist/package.json +3 -2
- package/dist/source/commands/export.js +3 -2
- package/dist/source/utils/close-ora-on-sigint.d.ts +2 -0
- package/dist/source/utils/close-ora-on-sigint.js +9 -0
- package/dist/source/utils/download-client.js +1 -1
- package/dist/source/utils/generate-email-preview.d.ts +1 -1
- package/dist/source/utils/generate-email-preview.js +37 -7
- package/dist/source/utils/install-dependencies.js +2 -0
- package/dist/source/utils/start-server-command.js +31 -3
- package/dist/source/utils/watcher.js +9 -4
- package/package.json +3 -2
- package/source/commands/dev.ts +29 -0
- package/source/commands/export.ts +76 -0
- package/source/commands/preview.ts +44 -0
- package/source/index.ts +44 -0
- package/source/utils/close-ora-on-sigint.ts +7 -0
- package/source/utils/constants.ts +25 -0
- package/source/utils/convert-to-absolute-path.ts +4 -0
- package/source/utils/download-client.ts +28 -0
- package/source/utils/generate-email-preview.ts +106 -0
- package/source/utils/index.ts +8 -0
- package/source/utils/install-dependencies.ts +20 -0
- package/source/utils/run-server.ts +58 -0
- package/source/utils/start-server-command.ts +54 -0
- package/source/utils/sync-package.ts +20 -0
- package/source/utils/watcher.ts +64 -0
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"email": "./dist/source/index.js"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@types/normalize-path": "3.0.0",
|
|
51
51
|
"@types/shelljs": "0.8.11",
|
|
52
52
|
"prettier": "2.7.1",
|
|
53
|
-
"ts-node": "10.9.1"
|
|
53
|
+
"ts-node": "10.9.1",
|
|
54
|
+
"typescript": "5.0.4"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -38,14 +38,15 @@ const normalize_path_1 = __importDefault(require("normalize-path"));
|
|
|
38
38
|
const path_1 = __importDefault(require("path"));
|
|
39
39
|
const shelljs_1 = __importDefault(require("shelljs"));
|
|
40
40
|
const fs_2 = __importDefault(require("fs"));
|
|
41
|
+
const close_ora_on_sigint_1 = require("../utils/close-ora-on-sigint");
|
|
41
42
|
/*
|
|
42
43
|
This first builds all the templates using esbuild and then puts the output in the `.js`
|
|
43
44
|
files. Then these `.js` files are imported dynamically and rendered to `.html` files
|
|
44
45
|
using the `render` function.
|
|
45
46
|
*/
|
|
46
47
|
const exportTemplates = async (outDir, srcDir, options) => {
|
|
47
|
-
var _a;
|
|
48
48
|
const spinner = (0, ora_1.default)('Preparing files...\n').start();
|
|
49
|
+
(0, close_ora_on_sigint_1.closeOraOnSIGNIT)(spinner);
|
|
49
50
|
const allTemplates = glob_1.glob.sync((0, normalize_path_1.default)(path_1.default.join(srcDir, '*.{tsx,jsx}')));
|
|
50
51
|
esbuild_1.default.buildSync({
|
|
51
52
|
bundle: true,
|
|
@@ -58,7 +59,7 @@ const exportTemplates = async (outDir, srcDir, options) => {
|
|
|
58
59
|
absolute: true,
|
|
59
60
|
});
|
|
60
61
|
for (const template of allBuiltTemplates) {
|
|
61
|
-
const component = await
|
|
62
|
+
const component = await Promise.resolve(`${template}`).then(s => __importStar(require(s)));
|
|
62
63
|
const rendered = (0, render_1.render)(component.default({}), options);
|
|
63
64
|
const htmlPath = template.replace('.js', options.plainText ? '.txt' : '.html');
|
|
64
65
|
(0, fs_1.writeFileSync)(htmlPath, rendered);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.closeOraOnSIGNIT = void 0;
|
|
4
|
+
const closeOraOnSIGNIT = (spinner) => {
|
|
5
|
+
process.on('SIGINT', function () {
|
|
6
|
+
spinner.stop();
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
exports.closeOraOnSIGNIT = closeOraOnSIGNIT;
|
|
@@ -14,7 +14,7 @@ const downloadClient = async () => {
|
|
|
14
14
|
const downloadRes = await octokit.repos.downloadTarballArchive({
|
|
15
15
|
owner: 'resendlabs',
|
|
16
16
|
repo: 'react-email',
|
|
17
|
-
ref: 'v0.0.
|
|
17
|
+
ref: 'v0.0.13',
|
|
18
18
|
});
|
|
19
19
|
fs_1.default.mkdirSync('.react-email-temp');
|
|
20
20
|
const TAR_PATH = path_1.default.join('.react-email-temp', 'react-email.tar.gz');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const generateEmailsPreview: (emailDir: string) => Promise<void>;
|
|
1
|
+
export declare const generateEmailsPreview: (emailDir: string, type?: 'all' | 'static' | 'templates') => Promise<void>;
|
|
@@ -11,11 +11,18 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
11
11
|
const shelljs_1 = __importDefault(require("shelljs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
14
|
-
const
|
|
14
|
+
const glob_1 = __importDefault(require("glob"));
|
|
15
|
+
const close_ora_on_sigint_1 = require("./close-ora-on-sigint");
|
|
16
|
+
const generateEmailsPreview = async (emailDir, type = 'all') => {
|
|
15
17
|
try {
|
|
16
18
|
const spinner = (0, ora_1.default)('Generating emails preview').start();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
(0, close_ora_on_sigint_1.closeOraOnSIGNIT)(spinner);
|
|
20
|
+
if (type === 'all' || type === 'templates') {
|
|
21
|
+
await createEmailPreviews(emailDir);
|
|
22
|
+
}
|
|
23
|
+
if (type === 'all' || type === 'static') {
|
|
24
|
+
await createStaticFiles(emailDir);
|
|
25
|
+
}
|
|
19
26
|
spinner.stopAndPersist({
|
|
20
27
|
symbol: log_symbols_1.default.success,
|
|
21
28
|
text: 'Emails preview generated',
|
|
@@ -31,9 +38,32 @@ const createEmailPreviews = async (emailDir) => {
|
|
|
31
38
|
if (hasEmailsDirectory) {
|
|
32
39
|
await fs_1.default.promises.rm(constants_1.PACKAGE_EMAILS_PATH, { recursive: true });
|
|
33
40
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const list = glob_1.default.sync(path_1.default.join(emailDir, '/*.{jsx,tsx}'), {
|
|
42
|
+
absolute: true,
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* instead of copying all files, which would break and js/ts imports,
|
|
46
|
+
* we create placeholder files which just contain the following code:
|
|
47
|
+
*
|
|
48
|
+
* import Mail from '../../path/to/emails/my-template.tsx`
|
|
49
|
+
* export default Mail
|
|
50
|
+
*/
|
|
51
|
+
for (const absoluteSrcFilePath of list) {
|
|
52
|
+
const fileName = absoluteSrcFilePath.split('/').pop();
|
|
53
|
+
const targetFile = path_1.default.join(constants_1.PACKAGE_EMAILS_PATH, absoluteSrcFilePath.replace(emailDir, ''));
|
|
54
|
+
const importPath = path_1.default.relative(path_1.default.dirname(targetFile), path_1.default.dirname(absoluteSrcFilePath));
|
|
55
|
+
const importFile = path_1.default.join(importPath, fileName);
|
|
56
|
+
// if this import is changed, you also need to update `client/src/app/preview/[slug]/page.tsx`
|
|
57
|
+
const sourceCode = `import Mail from '${importFile}';export default Mail;`.replace(';', ';\n');
|
|
58
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(targetFile));
|
|
59
|
+
if (fs_extra_1.default.existsSync(targetFile)) {
|
|
60
|
+
if (fs_extra_1.default.readFileSync(targetFile, 'utf8') === sourceCode) {
|
|
61
|
+
// file already exists, no need to trigger a rebuild.
|
|
62
|
+
// can otherwise trigger the next.js rebuild multiple times
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await fs_extra_1.default.writeFile(targetFile, sourceCode);
|
|
37
67
|
}
|
|
38
68
|
};
|
|
39
69
|
const createStaticFiles = async (emailDir) => {
|
|
@@ -42,7 +72,7 @@ const createStaticFiles = async (emailDir) => {
|
|
|
42
72
|
await fs_1.default.promises.rm(constants_1.PACKAGE_PUBLIC_PATH, { recursive: true });
|
|
43
73
|
}
|
|
44
74
|
await fs_extra_1.default.ensureDir(path_1.default.join(constants_1.PACKAGE_PUBLIC_PATH, 'static'));
|
|
45
|
-
const result = shelljs_1.default.cp('-r', path_1.default.join('static'), path_1.default.join(constants_1.PACKAGE_PUBLIC_PATH));
|
|
75
|
+
const result = shelljs_1.default.cp('-r', path_1.default.join(emailDir, 'static'), path_1.default.join(constants_1.PACKAGE_PUBLIC_PATH));
|
|
46
76
|
if (result.code > 0) {
|
|
47
77
|
throw new Error(`Something went wrong while copying the file to ${path_1.default.join(emailDir, 'static')}, ${result.cat()}`);
|
|
48
78
|
}
|
|
@@ -9,8 +9,10 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const constants_1 = require("./constants");
|
|
10
10
|
const ora_1 = __importDefault(require("ora"));
|
|
11
11
|
const log_symbols_1 = __importDefault(require("log-symbols"));
|
|
12
|
+
const close_ora_on_sigint_1 = require("./close-ora-on-sigint");
|
|
12
13
|
const installDependencies = async (packageManager) => {
|
|
13
14
|
const spinner = (0, ora_1.default)('Installing dependencies...\n').start();
|
|
15
|
+
(0, close_ora_on_sigint_1.closeOraOnSIGNIT)(spinner);
|
|
14
16
|
shelljs_1.default.cd(path_1.default.join(constants_1.REACT_EMAIL_ROOT));
|
|
15
17
|
shelljs_1.default.exec(`${packageManager} install`);
|
|
16
18
|
spinner.stopAndPersist({
|
|
@@ -5,19 +5,47 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.buildProdServer = exports.startProdServer = exports.startDevServer = void 0;
|
|
7
7
|
const shelljs_1 = __importDefault(require("shelljs"));
|
|
8
|
+
let processesToKill = [];
|
|
9
|
+
function execAsync(command) {
|
|
10
|
+
const process = shelljs_1.default.exec(command, { async: true });
|
|
11
|
+
processesToKill.push(process);
|
|
12
|
+
process.on('close', () => {
|
|
13
|
+
processesToKill = processesToKill.filter((p) => p !== process);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
8
16
|
const startDevServer = (packageManager, port) => {
|
|
9
|
-
|
|
17
|
+
execAsync(`${packageManager} run dev -- -p ${port}`);
|
|
10
18
|
};
|
|
11
19
|
exports.startDevServer = startDevServer;
|
|
12
20
|
const startProdServer = (packageManager, port) => {
|
|
13
|
-
|
|
21
|
+
execAsync(`${packageManager} run start -- -p ${port}`);
|
|
14
22
|
};
|
|
15
23
|
exports.startProdServer = startProdServer;
|
|
16
24
|
const buildProdServer = (packageManager) => {
|
|
17
|
-
|
|
25
|
+
execAsync(`${packageManager} run build`);
|
|
18
26
|
// if build fails for whatever reason, make sure the shell actually exits
|
|
19
27
|
process.on('close', (code) => {
|
|
20
28
|
shelljs_1.default.exit(code ?? undefined);
|
|
21
29
|
});
|
|
22
30
|
};
|
|
23
31
|
exports.buildProdServer = buildProdServer;
|
|
32
|
+
// based on https://stackoverflow.com/a/14032965
|
|
33
|
+
function exitHandler() {
|
|
34
|
+
if (processesToKill.length > 0) {
|
|
35
|
+
console.log('shutting down %d subprocesses', processesToKill.length);
|
|
36
|
+
}
|
|
37
|
+
processesToKill.forEach((p) => {
|
|
38
|
+
if (p.connected) {
|
|
39
|
+
p.kill();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// do something when app is closing
|
|
44
|
+
process.on('exit', exitHandler);
|
|
45
|
+
// catches ctrl+c event
|
|
46
|
+
process.on('SIGINT', exitHandler);
|
|
47
|
+
// catches "kill pid" (for example: nodemon restart)
|
|
48
|
+
process.on('SIGUSR1', exitHandler);
|
|
49
|
+
process.on('SIGUSR2', exitHandler);
|
|
50
|
+
// catches uncaught exceptions
|
|
51
|
+
process.on('uncaughtException', exitHandler);
|
|
@@ -5,10 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.watcher = exports.createWatcherInstance = void 0;
|
|
7
7
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
8
|
-
const constants_1 = require("./constants");
|
|
9
8
|
const fs_1 = __importDefault(require("fs"));
|
|
10
9
|
const path_1 = __importDefault(require("path"));
|
|
11
10
|
const shelljs_1 = __importDefault(require("shelljs"));
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const generate_email_preview_1 = require("./generate-email-preview");
|
|
12
13
|
const createWatcherInstance = (watchDir) => chokidar_1.default.watch(watchDir, {
|
|
13
14
|
ignoreInitial: true,
|
|
14
15
|
cwd: watchDir.split(path_1.default.sep).slice(0, -1).join(path_1.default.sep),
|
|
@@ -37,9 +38,13 @@ const watcher = (watcherInstance, watchDir) => {
|
|
|
37
38
|
}
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
try {
|
|
42
|
+
await (0, generate_email_preview_1.generateEmailsPreview)(watchDir, 'templates');
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new Error(`Something went wrong while copying the file to ${constants_1.PACKAGE_EMAILS_PATH}, ${
|
|
46
|
+
// @ts-expect-error
|
|
47
|
+
e?.message}`);
|
|
43
48
|
}
|
|
44
49
|
});
|
|
45
50
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"email": "./dist/source/index.js"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@types/normalize-path": "3.0.0",
|
|
51
51
|
"@types/shelljs": "0.8.11",
|
|
52
52
|
"prettier": "2.7.1",
|
|
53
|
-
"ts-node": "10.9.1"
|
|
53
|
+
"ts-node": "10.9.1",
|
|
54
|
+
"typescript": "5.0.4"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { downloadClient, REACT_EMAIL_ROOT } from '../utils';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import shell from 'shelljs';
|
|
4
|
+
import { setupServer } from '../utils/run-server';
|
|
5
|
+
|
|
6
|
+
interface Args {
|
|
7
|
+
dir: string;
|
|
8
|
+
port: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const dev = async ({ dir, port }: Args) => {
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(dir)) {
|
|
14
|
+
throw new Error(`Missing ${dir} folder`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(REACT_EMAIL_ROOT)) {
|
|
18
|
+
await setupServer('dev', dir, port);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await downloadClient();
|
|
23
|
+
|
|
24
|
+
await setupServer('dev', dir, port);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.log(error);
|
|
27
|
+
shell.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
import esbuild from 'esbuild';
|
|
3
|
+
import tree from 'tree-node-cli';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import logSymbols from 'log-symbols';
|
|
6
|
+
import { render, Options } from '@react-email/render';
|
|
7
|
+
import { unlinkSync, writeFileSync } from 'fs';
|
|
8
|
+
import normalize from 'normalize-path';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import shell from 'shelljs';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import { closeOraOnSIGNIT } from '../utils/close-ora-on-sigint';
|
|
13
|
+
/*
|
|
14
|
+
This first builds all the templates using esbuild and then puts the output in the `.js`
|
|
15
|
+
files. Then these `.js` files are imported dynamically and rendered to `.html` files
|
|
16
|
+
using the `render` function.
|
|
17
|
+
*/
|
|
18
|
+
export const exportTemplates = async (
|
|
19
|
+
outDir: string,
|
|
20
|
+
srcDir: string,
|
|
21
|
+
options: Options,
|
|
22
|
+
) => {
|
|
23
|
+
const spinner = ora('Preparing files...\n').start();
|
|
24
|
+
closeOraOnSIGNIT(spinner)
|
|
25
|
+
|
|
26
|
+
const allTemplates = glob.sync(normalize(path.join(srcDir, '*.{tsx,jsx}')));
|
|
27
|
+
|
|
28
|
+
esbuild.buildSync({
|
|
29
|
+
bundle: true,
|
|
30
|
+
entryPoints: allTemplates,
|
|
31
|
+
platform: 'node',
|
|
32
|
+
write: true,
|
|
33
|
+
outdir: outDir,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const allBuiltTemplates = glob.sync(normalize(`${outDir}/*.js`), {
|
|
37
|
+
absolute: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const template of allBuiltTemplates) {
|
|
41
|
+
const component = await import(template);
|
|
42
|
+
const rendered = render(component.default({}), options);
|
|
43
|
+
const htmlPath = template.replace(
|
|
44
|
+
'.js',
|
|
45
|
+
options.plainText ? '.txt' : '.html',
|
|
46
|
+
);
|
|
47
|
+
writeFileSync(htmlPath, rendered);
|
|
48
|
+
unlinkSync(template);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const staticDir = path.join(srcDir, 'static');
|
|
52
|
+
const hasStaticDirectory = fs.existsSync(staticDir);
|
|
53
|
+
|
|
54
|
+
if (hasStaticDirectory) {
|
|
55
|
+
const result = shell.cp('-r', staticDir, path.join(outDir, 'static'));
|
|
56
|
+
if (result.code > 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Something went wrong while copying the file to ${outDir}/static, ${result.cat()}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fileTree = tree(outDir, {
|
|
64
|
+
allFiles: true,
|
|
65
|
+
maxDepth: 4,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(fileTree);
|
|
69
|
+
|
|
70
|
+
spinner.stopAndPersist({
|
|
71
|
+
symbol: logSymbols.success,
|
|
72
|
+
text: 'Successfully exported emails',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
process.exit();
|
|
76
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import shell from 'shelljs';
|
|
3
|
+
import { downloadClient, REACT_EMAIL_ROOT } from '../utils';
|
|
4
|
+
import { setupServer } from '../utils/run-server';
|
|
5
|
+
|
|
6
|
+
interface BuildPreviewArgs {
|
|
7
|
+
dir: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const buildPreview = async ({ dir }: BuildPreviewArgs) => {
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(REACT_EMAIL_ROOT)) {
|
|
13
|
+
await setupServer('build', dir, '');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await downloadClient();
|
|
18
|
+
|
|
19
|
+
await setupServer('build', dir, '');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.log(error);
|
|
22
|
+
shell.exit(1);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface StartPreviewArgs {
|
|
27
|
+
port: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const startPreview = async ({ port }: StartPreviewArgs) => {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(REACT_EMAIL_ROOT)) {
|
|
33
|
+
await setupServer('start', '', port);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await downloadClient();
|
|
38
|
+
|
|
39
|
+
await setupServer('start', '', port);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log(error);
|
|
42
|
+
shell.exit(1);
|
|
43
|
+
}
|
|
44
|
+
};
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from '@commander-js/extra-typings';
|
|
3
|
+
import { dev } from './commands/dev';
|
|
4
|
+
import { buildPreview, startPreview } from './commands/preview';
|
|
5
|
+
import { exportTemplates } from './commands/export';
|
|
6
|
+
import { PACKAGE_NAME } from './utils/constants';
|
|
7
|
+
import packageJson from '../package.json';
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name(PACKAGE_NAME)
|
|
11
|
+
.description('A live preview of your emails right in your browser')
|
|
12
|
+
.version(packageJson.version);
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('dev')
|
|
16
|
+
.description('Starts the application in development mode')
|
|
17
|
+
.option('-d, --dir <path>', 'Directory with your email templates', './emails')
|
|
18
|
+
.option('-p --port <port>', 'Port to run dev server on', '3000')
|
|
19
|
+
.action(dev);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('build')
|
|
23
|
+
.description('Builds a production preview app')
|
|
24
|
+
.option('-d, --dir <path>', 'Directory with your email templates', './emails')
|
|
25
|
+
.action(buildPreview);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('start')
|
|
29
|
+
.description('Starts the production build of the preview app')
|
|
30
|
+
.option('-p --port <port>', 'Port to run production server on', '3000')
|
|
31
|
+
.action(startPreview);
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('export')
|
|
35
|
+
.description('Build the templates to the `out` directory')
|
|
36
|
+
.option('--outDir <path>', 'Output directory', 'out')
|
|
37
|
+
.option('-p, --pretty', 'Pretty print the output', false)
|
|
38
|
+
.option('-t, --plainText', 'Set output format as plain text', false)
|
|
39
|
+
.option('-d, --dir <path>', 'Directory with your email templates', './emails')
|
|
40
|
+
.action(({ outDir, pretty, plainText, dir: srcDir }) =>
|
|
41
|
+
exportTemplates(outDir, srcDir, { pretty, plainText }),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
program.parse();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
// Package variables
|
|
4
|
+
export const DEFAULT_EMAILS_DIRECTORY = 'emails';
|
|
5
|
+
export const PACKAGE_NAME = 'react-email';
|
|
6
|
+
|
|
7
|
+
// Default paths
|
|
8
|
+
export const CURRENT_PATH = process.cwd();
|
|
9
|
+
|
|
10
|
+
// User paths
|
|
11
|
+
export const USER_PACKAGE_JSON = path.join(CURRENT_PATH, 'package.json');
|
|
12
|
+
export const USER_STATIC_FILES = path.join(CURRENT_PATH, 'emails', 'static');
|
|
13
|
+
|
|
14
|
+
// React Email paths
|
|
15
|
+
export const REACT_EMAIL_ROOT = path.join(CURRENT_PATH, '.react-email');
|
|
16
|
+
|
|
17
|
+
// Events
|
|
18
|
+
export const EVENT_FILE_DELETED = 'unlink';
|
|
19
|
+
|
|
20
|
+
export const PACKAGE_EMAILS_PATH = path.join(
|
|
21
|
+
REACT_EMAIL_ROOT,
|
|
22
|
+
DEFAULT_EMAILS_DIRECTORY,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const PACKAGE_PUBLIC_PATH = path.join(REACT_EMAIL_ROOT, 'public');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import shell from 'shelljs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export const downloadClient = async () => {
|
|
8
|
+
const octokit = new Octokit();
|
|
9
|
+
const downloadRes = await octokit.repos.downloadTarballArchive({
|
|
10
|
+
owner: 'resendlabs',
|
|
11
|
+
repo: 'react-email',
|
|
12
|
+
ref: 'v0.0.13',
|
|
13
|
+
});
|
|
14
|
+
fs.mkdirSync('.react-email-temp');
|
|
15
|
+
const TAR_PATH = path.join('.react-email-temp', 'react-email.tar.gz');
|
|
16
|
+
fs.writeFileSync(TAR_PATH, Buffer.from(downloadRes.data as any));
|
|
17
|
+
shell.exec(
|
|
18
|
+
`tar -xzvf .react-email-temp/react-email.tar.gz -C .react-email-temp --strip-components 1`,
|
|
19
|
+
{ silent: true },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
fse.moveSync(
|
|
23
|
+
path.join('.react-email-temp', 'client'),
|
|
24
|
+
path.join('.react-email'),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
fse.removeSync('.react-email-temp');
|
|
28
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logSymbols from 'log-symbols';
|
|
2
|
+
import { PACKAGE_EMAILS_PATH, PACKAGE_PUBLIC_PATH } from './constants';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import shell from 'shelljs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fse from 'fs-extra';
|
|
8
|
+
import glob from 'glob';
|
|
9
|
+
import { closeOraOnSIGNIT } from './close-ora-on-sigint';
|
|
10
|
+
|
|
11
|
+
export const generateEmailsPreview = async (
|
|
12
|
+
emailDir: string,
|
|
13
|
+
type: 'all' | 'static' | 'templates' = 'all',
|
|
14
|
+
) => {
|
|
15
|
+
try {
|
|
16
|
+
const spinner = ora('Generating emails preview').start();
|
|
17
|
+
closeOraOnSIGNIT(spinner)
|
|
18
|
+
|
|
19
|
+
if (type === 'all' || type === 'templates') {
|
|
20
|
+
await createEmailPreviews(emailDir);
|
|
21
|
+
}
|
|
22
|
+
if (type === 'all' || type === 'static') {
|
|
23
|
+
await createStaticFiles(emailDir);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
spinner.stopAndPersist({
|
|
27
|
+
symbol: logSymbols.success,
|
|
28
|
+
text: 'Emails preview generated',
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.log({ error });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const createEmailPreviews = async (emailDir: string) => {
|
|
36
|
+
const hasEmailsDirectory = fs.existsSync(PACKAGE_EMAILS_PATH);
|
|
37
|
+
|
|
38
|
+
if (hasEmailsDirectory) {
|
|
39
|
+
await fs.promises.rm(PACKAGE_EMAILS_PATH, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const list = glob.sync(path.join(emailDir, '/*.{jsx,tsx}'), {
|
|
43
|
+
absolute: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* instead of copying all files, which would break and js/ts imports,
|
|
48
|
+
* we create placeholder files which just contain the following code:
|
|
49
|
+
*
|
|
50
|
+
* import Mail from '../../path/to/emails/my-template.tsx`
|
|
51
|
+
* export default Mail
|
|
52
|
+
*/
|
|
53
|
+
for (const absoluteSrcFilePath of list) {
|
|
54
|
+
const fileName = absoluteSrcFilePath.split('/').pop()!;
|
|
55
|
+
const targetFile = path.join(
|
|
56
|
+
PACKAGE_EMAILS_PATH,
|
|
57
|
+
absoluteSrcFilePath.replace(emailDir, ''),
|
|
58
|
+
);
|
|
59
|
+
const importPath = path.relative(
|
|
60
|
+
path.dirname(targetFile),
|
|
61
|
+
path.dirname(absoluteSrcFilePath),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const importFile = path.join(importPath, fileName);
|
|
65
|
+
|
|
66
|
+
// if this import is changed, you also need to update `client/src/app/preview/[slug]/page.tsx`
|
|
67
|
+
const sourceCode =
|
|
68
|
+
`import Mail from '${importFile}';export default Mail;`.replace(
|
|
69
|
+
';',
|
|
70
|
+
';\n',
|
|
71
|
+
);
|
|
72
|
+
await fse.ensureDir(path.dirname(targetFile));
|
|
73
|
+
if (fse.existsSync(targetFile)) {
|
|
74
|
+
if (fse.readFileSync(targetFile, 'utf8') === sourceCode) {
|
|
75
|
+
// file already exists, no need to trigger a rebuild.
|
|
76
|
+
// can otherwise trigger the next.js rebuild multiple times
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await fse.writeFile(targetFile, sourceCode);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const createStaticFiles = async (emailDir: string) => {
|
|
85
|
+
const hasPublicDirectory = fs.existsSync(PACKAGE_PUBLIC_PATH);
|
|
86
|
+
|
|
87
|
+
if (hasPublicDirectory) {
|
|
88
|
+
await fs.promises.rm(PACKAGE_PUBLIC_PATH, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fse.ensureDir(path.join(PACKAGE_PUBLIC_PATH, 'static'));
|
|
92
|
+
|
|
93
|
+
const result = shell.cp(
|
|
94
|
+
'-r',
|
|
95
|
+
path.join(emailDir, 'static'),
|
|
96
|
+
path.join(PACKAGE_PUBLIC_PATH),
|
|
97
|
+
);
|
|
98
|
+
if (result.code > 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Something went wrong while copying the file to ${path.join(
|
|
101
|
+
emailDir,
|
|
102
|
+
'static',
|
|
103
|
+
)}, ${result.cat()}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './constants';
|
|
2
|
+
export * from './convert-to-absolute-path';
|
|
3
|
+
export * from './download-client';
|
|
4
|
+
export * from './generate-email-preview';
|
|
5
|
+
export * from './install-dependencies';
|
|
6
|
+
export * from './start-server-command';
|
|
7
|
+
export * from './sync-package';
|
|
8
|
+
export * from './watcher';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import shell from 'shelljs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { REACT_EMAIL_ROOT } from './constants';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import logSymbols from 'log-symbols';
|
|
6
|
+
import { closeOraOnSIGNIT } from './close-ora-on-sigint';
|
|
7
|
+
|
|
8
|
+
export type PackageManager = 'yarn' | 'npm' | 'pnpm';
|
|
9
|
+
|
|
10
|
+
export const installDependencies = async (packageManager: PackageManager) => {
|
|
11
|
+
const spinner = ora('Installing dependencies...\n').start();
|
|
12
|
+
closeOraOnSIGNIT(spinner)
|
|
13
|
+
|
|
14
|
+
shell.cd(path.join(REACT_EMAIL_ROOT));
|
|
15
|
+
shell.exec(`${packageManager} install`);
|
|
16
|
+
spinner.stopAndPersist({
|
|
17
|
+
symbol: logSymbols.success,
|
|
18
|
+
text: 'Dependencies installed',
|
|
19
|
+
});
|
|
20
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createWatcherInstance, watcher } from './watcher';
|
|
2
|
+
import { detect as detectPackageManager } from 'detect-package-manager';
|
|
3
|
+
import { findRoot } from '@manypkg/find-root';
|
|
4
|
+
import {
|
|
5
|
+
CURRENT_PATH,
|
|
6
|
+
convertToAbsolutePath,
|
|
7
|
+
startDevServer,
|
|
8
|
+
installDependencies,
|
|
9
|
+
PackageManager,
|
|
10
|
+
syncPkg,
|
|
11
|
+
generateEmailsPreview,
|
|
12
|
+
buildProdServer,
|
|
13
|
+
startProdServer,
|
|
14
|
+
REACT_EMAIL_ROOT,
|
|
15
|
+
} from '.';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import shell from 'shelljs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Utility function to run init/sync for the server in dev, build or start mode.
|
|
21
|
+
*
|
|
22
|
+
* @param type dev | build | start
|
|
23
|
+
* @param dir Directory in which the emails are located, only for dev and build, unused for start.
|
|
24
|
+
* @param port The port on which the server will run, only for dev and start, unused for build.
|
|
25
|
+
*/
|
|
26
|
+
export const setupServer = async (
|
|
27
|
+
type: 'dev' | 'build' | 'start',
|
|
28
|
+
dir: string,
|
|
29
|
+
port: string,
|
|
30
|
+
) => {
|
|
31
|
+
const cwd = await findRoot(CURRENT_PATH).catch(() => ({
|
|
32
|
+
rootDir: CURRENT_PATH,
|
|
33
|
+
}));
|
|
34
|
+
const emailDir = convertToAbsolutePath(dir);
|
|
35
|
+
const packageManager: PackageManager = await detectPackageManager({
|
|
36
|
+
cwd: cwd.rootDir,
|
|
37
|
+
}).catch(() => 'npm');
|
|
38
|
+
|
|
39
|
+
// when starting, we dont need to worry about these because it should've already happened during the build stage.
|
|
40
|
+
if (type !== 'start') {
|
|
41
|
+
await generateEmailsPreview(emailDir);
|
|
42
|
+
await syncPkg();
|
|
43
|
+
await installDependencies(packageManager);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (type === 'dev') {
|
|
47
|
+
const watcherInstance = createWatcherInstance(emailDir);
|
|
48
|
+
|
|
49
|
+
startDevServer(packageManager, port);
|
|
50
|
+
watcher(watcherInstance, emailDir);
|
|
51
|
+
} else if (type === 'build') {
|
|
52
|
+
buildProdServer(packageManager);
|
|
53
|
+
} else {
|
|
54
|
+
shell.cd(path.join(REACT_EMAIL_ROOT));
|
|
55
|
+
|
|
56
|
+
startProdServer(packageManager, port);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import shell from 'shelljs';
|
|
3
|
+
|
|
4
|
+
let processesToKill: ChildProcess[] = [];
|
|
5
|
+
|
|
6
|
+
function execAsync(command: string) {
|
|
7
|
+
const process = shell.exec(command, { async: true });
|
|
8
|
+
processesToKill.push(process);
|
|
9
|
+
process.on('close', () => {
|
|
10
|
+
processesToKill = processesToKill.filter((p) => p !== process);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const startDevServer = (packageManager: string, port: string) => {
|
|
15
|
+
execAsync(`${packageManager} run dev -- -p ${port}`);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const startProdServer = (packageManager: string, port: string) => {
|
|
19
|
+
execAsync(`${packageManager} run start -- -p ${port}`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const buildProdServer = (packageManager: string) => {
|
|
23
|
+
execAsync(`${packageManager} run build`);
|
|
24
|
+
|
|
25
|
+
// if build fails for whatever reason, make sure the shell actually exits
|
|
26
|
+
process.on('close', (code) => {
|
|
27
|
+
shell.exit(code ?? undefined);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// based on https://stackoverflow.com/a/14032965
|
|
32
|
+
function exitHandler() {
|
|
33
|
+
if (processesToKill.length > 0) {
|
|
34
|
+
console.log('shutting down %d subprocesses', processesToKill.length);
|
|
35
|
+
}
|
|
36
|
+
processesToKill.forEach((p) => {
|
|
37
|
+
if (p.connected) {
|
|
38
|
+
p.kill();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// do something when app is closing
|
|
44
|
+
process.on('exit', exitHandler);
|
|
45
|
+
|
|
46
|
+
// catches ctrl+c event
|
|
47
|
+
process.on('SIGINT', exitHandler);
|
|
48
|
+
|
|
49
|
+
// catches "kill pid" (for example: nodemon restart)
|
|
50
|
+
process.on('SIGUSR1', exitHandler);
|
|
51
|
+
process.on('SIGUSR2', exitHandler);
|
|
52
|
+
|
|
53
|
+
// catches uncaught exceptions
|
|
54
|
+
process.on('uncaughtException', exitHandler);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import readPackage from 'read-pkg';
|
|
2
|
+
import { REACT_EMAIL_ROOT } from './constants';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export const syncPkg = async () => {
|
|
7
|
+
const clientPkg = await readPackage({ cwd: REACT_EMAIL_ROOT });
|
|
8
|
+
const userPkg = await readPackage();
|
|
9
|
+
const pkg = {
|
|
10
|
+
...clientPkg,
|
|
11
|
+
dependencies: {
|
|
12
|
+
...clientPkg.dependencies,
|
|
13
|
+
...userPkg.dependencies,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
await fs.promises.writeFile(
|
|
17
|
+
path.join(REACT_EMAIL_ROOT, 'package.json'),
|
|
18
|
+
JSON.stringify(pkg),
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chokidar, { FSWatcher } from 'chokidar';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import shell from 'shelljs';
|
|
5
|
+
import {
|
|
6
|
+
EVENT_FILE_DELETED,
|
|
7
|
+
PACKAGE_EMAILS_PATH,
|
|
8
|
+
REACT_EMAIL_ROOT,
|
|
9
|
+
} from './constants';
|
|
10
|
+
import { generateEmailsPreview } from './generate-email-preview';
|
|
11
|
+
|
|
12
|
+
export const createWatcherInstance = (watchDir: string) =>
|
|
13
|
+
chokidar.watch(watchDir, {
|
|
14
|
+
ignoreInitial: true,
|
|
15
|
+
cwd: watchDir.split(path.sep).slice(0, -1).join(path.sep),
|
|
16
|
+
ignored: /(^|[\/\\])\../,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const watcher = (watcherInstance: FSWatcher, watchDir: string) => {
|
|
20
|
+
watcherInstance.on('all', async (event, filename) => {
|
|
21
|
+
const file = filename.split(path.sep);
|
|
22
|
+
if (file[1] === undefined) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (event === EVENT_FILE_DELETED) {
|
|
27
|
+
if (file[1] === 'static' && file[2]) {
|
|
28
|
+
await fs.promises.rm(
|
|
29
|
+
path.join(REACT_EMAIL_ROOT, 'public', 'static', file[2]),
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await fs.promises.rm(path.join(REACT_EMAIL_ROOT, filename));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (file[1] === 'static' && file[2]) {
|
|
39
|
+
const srcPath = path.join(watchDir, 'static', file[2]);
|
|
40
|
+
const result = shell.cp(
|
|
41
|
+
'-r',
|
|
42
|
+
srcPath,
|
|
43
|
+
path.join(REACT_EMAIL_ROOT, 'public', 'static'),
|
|
44
|
+
);
|
|
45
|
+
if (result.code > 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Something went wrong while copying the file to ${PACKAGE_EMAILS_PATH}, ${result.cat()}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await generateEmailsPreview(watchDir, 'templates');
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Something went wrong while copying the file to ${PACKAGE_EMAILS_PATH}, ${
|
|
58
|
+
// @ts-expect-error
|
|
59
|
+
e?.message
|
|
60
|
+
}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|