@depup/react-email 5.2.10-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +652 -0
  2. package/README.md +43 -0
  3. package/changes.json +58 -0
  4. package/dev/CHANGELOG.md +13 -0
  5. package/dev/index.js +46 -0
  6. package/dev/package.json +10 -0
  7. package/dist/index.mjs +7326 -0
  8. package/license.md +7 -0
  9. package/package.json +128 -0
  10. package/readme.md +59 -0
  11. package/src/commands/build.ts +269 -0
  12. package/src/commands/dev.ts +27 -0
  13. package/src/commands/export.ts +204 -0
  14. package/src/commands/resend/reset.ts +8 -0
  15. package/src/commands/resend/setup.ts +29 -0
  16. package/src/commands/start.ts +38 -0
  17. package/src/index.ts +110 -0
  18. package/src/utils/conf.ts +9 -0
  19. package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
  20. package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
  21. package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
  22. package/src/utils/get-emails-directory-metadata.ts +140 -0
  23. package/src/utils/get-preview-server-location.ts +50 -0
  24. package/src/utils/index.ts +2 -0
  25. package/src/utils/packageJson.ts +4 -0
  26. package/src/utils/preview/get-env-variables-for-preview-app.ts +20 -0
  27. package/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts +226 -0
  28. package/src/utils/preview/hot-reloading/create-dependency-graph.ts +343 -0
  29. package/src/utils/preview/hot-reloading/get-imported-modules.spec.ts +151 -0
  30. package/src/utils/preview/hot-reloading/get-imported-modules.ts +49 -0
  31. package/src/utils/preview/hot-reloading/resolve-path-aliases.spec.ts +11 -0
  32. package/src/utils/preview/hot-reloading/resolve-path-aliases.ts +32 -0
  33. package/src/utils/preview/hot-reloading/setup-hot-reloading.ts +121 -0
  34. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/data-to-import.json +1 -0
  35. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/file-a.ts +5 -0
  36. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/file-b.ts +5 -0
  37. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/general-importing-file.ts +9 -0
  38. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/outer-dependency.ts +3 -0
  39. package/src/utils/preview/hot-reloading/test/dependency-graph/inner/path-aliases.ts +1 -0
  40. package/src/utils/preview/hot-reloading/test/dependency-graph/outer.ts +5 -0
  41. package/src/utils/preview/hot-reloading/test/some-file.ts +0 -0
  42. package/src/utils/preview/hot-reloading/test/tsconfig.json +8 -0
  43. package/src/utils/preview/index.ts +2 -0
  44. package/src/utils/preview/serve-static-file.ts +51 -0
  45. package/src/utils/preview/start-dev-server.ts +252 -0
  46. package/src/utils/register-spinner-autostopping.ts +28 -0
  47. package/src/utils/style-text.ts +11 -0
  48. package/src/utils/tree.spec.ts +29 -0
  49. package/src/utils/tree.ts +76 -0
  50. package/src/utils/types/hot-reload-change.ts +6 -0
  51. package/src/utils/types/hot-reload-event.ts +3 -0
  52. package/tsconfig.json +39 -0
  53. package/tsdown.config.ts +8 -0
  54. package/vitest.config.ts +15 -0
@@ -0,0 +1,252 @@
1
+ import http from 'node:http';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import url from 'node:url';
5
+ import { createJiti } from 'jiti';
6
+ import logSymbols from 'log-symbols';
7
+ import ora from 'ora';
8
+ import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping.js';
9
+ import { conf } from '../conf.js';
10
+ import { getPreviewServerLocation } from '../get-preview-server-location.js';
11
+ import { packageJson } from '../packageJson.js';
12
+ import { styleText } from '../style-text.js';
13
+ import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app.js';
14
+ import { serveStaticFile } from './serve-static-file.js';
15
+
16
+ let devServer: http.Server | undefined;
17
+
18
+ const safeAsyncServerListen = (server: http.Server, port: number) => {
19
+ return new Promise<{ portAlreadyInUse: boolean }>((resolve) => {
20
+ server.listen(port, () => {
21
+ resolve({ portAlreadyInUse: false });
22
+ });
23
+
24
+ server.on('error', (e: NodeJS.ErrnoException) => {
25
+ if (e.code === 'EADDRINUSE') {
26
+ resolve({ portAlreadyInUse: true });
27
+ }
28
+ });
29
+ });
30
+ };
31
+
32
+ export const startDevServer = async (
33
+ emailsDirRelativePath: string,
34
+ staticBaseDirRelativePath: string,
35
+ port: number,
36
+ ): Promise<http.Server> => {
37
+ const [majorNodeVersion] = process.versions.node.split('.');
38
+ if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) {
39
+ console.error(
40
+ ` ${logSymbols.error} Node ${majorNodeVersion} is not supported. Please upgrade to Node 20 or higher.`,
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ const previewServerLocation = await getPreviewServerLocation();
46
+ const previewServer = createJiti(previewServerLocation);
47
+
48
+ devServer = http.createServer((req, res) => {
49
+ if (!req.url) {
50
+ res.end(404);
51
+ return;
52
+ }
53
+
54
+ const parsedUrl = url.parse(req.url, true);
55
+
56
+ // Never cache anything to avoid
57
+ res.setHeader(
58
+ 'Cache-Control',
59
+ 'no-cache, max-age=0, must-revalidate, no-store',
60
+ );
61
+ res.setHeader('Pragma', 'no-cache');
62
+ res.setHeader('Expires', '-1');
63
+
64
+ try {
65
+ if (
66
+ parsedUrl.path?.includes('static/') &&
67
+ !parsedUrl.path.includes('_next/static/')
68
+ ) {
69
+ void serveStaticFile(res, parsedUrl, staticBaseDirRelativePath);
70
+ } else if (!isNextReady) {
71
+ void nextReadyPromise.then(() =>
72
+ nextHandleRequest?.(req, res, parsedUrl),
73
+ );
74
+ } else {
75
+ void nextHandleRequest?.(req, res, parsedUrl);
76
+ }
77
+ } catch (e) {
78
+ console.error('caught error', e);
79
+
80
+ res.writeHead(500);
81
+ res.end();
82
+ }
83
+ });
84
+
85
+ const { portAlreadyInUse } = await safeAsyncServerListen(devServer, port);
86
+
87
+ if (!portAlreadyInUse) {
88
+ console.log(
89
+ styleText('greenBright', ` React Email ${packageJson.version}`),
90
+ );
91
+ console.log(` Running preview at: http://localhost:${port}\n`);
92
+ } else {
93
+ const nextPortToTry = port + 1;
94
+ console.warn(
95
+ ` ${logSymbols.warning} Port ${port} is already in use, trying ${nextPortToTry}`,
96
+ );
97
+ return startDevServer(
98
+ emailsDirRelativePath,
99
+ staticBaseDirRelativePath,
100
+ nextPortToTry,
101
+ );
102
+ }
103
+
104
+ devServer.on('close', async () => {
105
+ await app.close();
106
+ });
107
+
108
+ devServer.on('error', (e: NodeJS.ErrnoException) => {
109
+ spinner.stopAndPersist({
110
+ symbol: logSymbols.error,
111
+ text: `Preview Server had an error: ${e}`,
112
+ });
113
+ process.exit(1);
114
+ });
115
+
116
+ const spinner = ora({
117
+ text: 'Getting react-email preview server ready...\n',
118
+ prefixText: ' ',
119
+ }).start();
120
+
121
+ registerSpinnerAutostopping(spinner);
122
+ const timeBeforeNextReady = performance.now();
123
+
124
+ // these environment variables are used on the next app
125
+ // this is the most reliable way of communicating these paths through
126
+ process.env = {
127
+ NODE_ENV: 'development',
128
+ ...(process.env as Omit<NodeJS.ProcessEnv, 'NODE_ENV'> & {
129
+ NODE_ENV?: NodeJS.ProcessEnv['NODE_ENV'];
130
+ }),
131
+ ...getEnvVariablesForPreviewApp(
132
+ // If we don't do normalization here, stuff like https://github.com/resend/react-email/issues/1354 happens.
133
+ path.normalize(emailsDirRelativePath),
134
+ previewServerLocation,
135
+ process.cwd(),
136
+ conf.get('resendApiKey'),
137
+ ),
138
+ };
139
+ if (!process.env.ESBUILD_BINARY_PATH) {
140
+ try {
141
+ const esbuild = createJiti(previewServer.esmResolve('esbuild'));
142
+ const subpath =
143
+ process.platform === 'win32' ? 'esbuild.exe' : 'bin/esbuild';
144
+ const esbuildBinaryPath = url.fileURLToPath(
145
+ esbuild.esmResolve(
146
+ `@esbuild/${process.platform}-${os.arch()}/${subpath}`,
147
+ ),
148
+ );
149
+ process.env.ESBUILD_BINARY_PATH = esbuildBinaryPath;
150
+ } catch (_exception) {
151
+ // Optional: platform binary may be missing; esbuild will use its default resolution.
152
+ }
153
+ }
154
+
155
+ const next = await previewServer.import<typeof import('next')['default']>(
156
+ 'next',
157
+ {
158
+ default: true,
159
+ },
160
+ );
161
+
162
+ const app = next({
163
+ dev: false,
164
+ conf: {
165
+ // passing in env here does not get the environment variables there
166
+ images: {
167
+ // This is to avoid the warning with sharp
168
+ unoptimized: true,
169
+ },
170
+ },
171
+ hostname: 'localhost',
172
+ port,
173
+ dir: previewServerLocation,
174
+ });
175
+
176
+ let isNextReady = false;
177
+ const nextReadyPromise = app.prepare();
178
+ try {
179
+ await nextReadyPromise;
180
+ } catch (exception) {
181
+ spinner.stopAndPersist({
182
+ symbol: logSymbols.error,
183
+ text: ` Preview Server had an error: ${exception}`,
184
+ });
185
+ process.exit(1);
186
+ }
187
+ isNextReady = true;
188
+
189
+ const nextHandleRequest:
190
+ | ReturnType<typeof app.getRequestHandler>
191
+ | undefined = app.getRequestHandler();
192
+
193
+ const secondsToNextReady = (
194
+ (performance.now() - timeBeforeNextReady) /
195
+ 1000
196
+ ).toFixed(1);
197
+
198
+ spinner.stopAndPersist({
199
+ text: `Ready in ${secondsToNextReady}s\n`,
200
+ symbol: logSymbols.success,
201
+ });
202
+
203
+ return devServer;
204
+ };
205
+
206
+ // based on https://stackoverflow.com/a/14032965
207
+ const makeExitHandler =
208
+ (
209
+ options?:
210
+ | { shouldKillProcess: false }
211
+ | { shouldKillProcess: true; killWithErrorCode: boolean },
212
+ ) =>
213
+ (codeSignalOrError: number | NodeJS.Signals | Error) => {
214
+ if (typeof devServer !== 'undefined') {
215
+ console.log('\nshutting down dev server');
216
+ devServer.close();
217
+ devServer = undefined;
218
+ }
219
+
220
+ if (codeSignalOrError instanceof Error) {
221
+ console.error(codeSignalOrError);
222
+ }
223
+
224
+ if (options?.shouldKillProcess) {
225
+ process.exit(options.killWithErrorCode ? 1 : 0);
226
+ }
227
+ };
228
+
229
+ // do something when app is closing
230
+ process.on('exit', makeExitHandler());
231
+
232
+ // catches ctrl+c event
233
+ process.on(
234
+ 'SIGINT',
235
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
236
+ );
237
+
238
+ // catches "kill pid" (for example: nodemon restart)
239
+ process.on(
240
+ 'SIGUSR1',
241
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
242
+ );
243
+ process.on(
244
+ 'SIGUSR2',
245
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
246
+ );
247
+
248
+ // catches uncaught exceptions
249
+ process.on(
250
+ 'uncaughtException',
251
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: true }),
252
+ );
@@ -0,0 +1,28 @@
1
+ import logSymbols from 'log-symbols';
2
+ import type { Ora } from 'ora';
3
+
4
+ const spinners = new Set<Ora>();
5
+
6
+ process.on('SIGINT', () => {
7
+ spinners.forEach((spinner) => {
8
+ if (spinner.isSpinning) {
9
+ spinner.stop();
10
+ }
11
+ });
12
+ });
13
+
14
+ process.on('exit', (code) => {
15
+ if (code !== 0) {
16
+ spinners.forEach((spinner) => {
17
+ if (spinner.isSpinning) {
18
+ spinner.stopAndPersist({
19
+ symbol: logSymbols.error,
20
+ });
21
+ }
22
+ });
23
+ }
24
+ });
25
+
26
+ export const registerSpinnerAutostopping = (spinner: Ora) => {
27
+ spinners.add(spinner);
28
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Centralized fallback for Node versions (<20.12.0) without util.styleText.
3
+ * Returns the original text when styleText is unavailable.
4
+ */
5
+ import * as nodeUtil from 'node:util';
6
+
7
+ type StyleTextFunction = typeof nodeUtil.styleText;
8
+
9
+ export const styleText: StyleTextFunction = (nodeUtil as any).styleText
10
+ ? (nodeUtil as any).styleText
11
+ : (_: string, text: string) => text;
@@ -0,0 +1,29 @@
1
+ import { tree } from './tree.js';
2
+
3
+ test('tree(__dirname, 2)', async () => {
4
+ expect(await tree(__dirname, 2)).toMatchInlineSnapshot(`
5
+ "utils
6
+ ├── esbuild
7
+ │ ├── escape-string-for-regex.ts
8
+ │ └── renderring-utilities-exporter.ts
9
+ ├── preview
10
+ │ ├── hot-reloading
11
+ │ ├── get-env-variables-for-preview-app.ts
12
+ │ ├── index.ts
13
+ │ ├── serve-static-file.ts
14
+ │ └── start-dev-server.ts
15
+ ├── types
16
+ │ ├── hot-reload-change.ts
17
+ │ └── hot-reload-event.ts
18
+ ├── conf.ts
19
+ ├── get-emails-directory-metadata.spec.ts
20
+ ├── get-emails-directory-metadata.ts
21
+ ├── get-preview-server-location.ts
22
+ ├── index.ts
23
+ ├── packageJson.ts
24
+ ├── register-spinner-autostopping.ts
25
+ ├── style-text.ts
26
+ ├── tree.spec.ts
27
+ └── tree.ts"
28
+ `);
29
+ });
@@ -0,0 +1,76 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const SYMBOLS = {
6
+ BRANCH: '├── ',
7
+ EMPTY: '',
8
+ INDENT: ' ',
9
+ LAST_BRANCH: '└── ',
10
+ VERTICAL: '│ ',
11
+ };
12
+
13
+ const getTreeLines = async (
14
+ dirPath: string,
15
+ depth: number,
16
+ currentDepth = 0,
17
+ ) => {
18
+ const base = process.cwd();
19
+ const dirFullpath = path.resolve(base, dirPath);
20
+ const dirname = path.basename(dirFullpath);
21
+ let lines = [dirname];
22
+
23
+ const dirStat = await fs.stat(dirFullpath);
24
+ if (dirStat.isDirectory() && currentDepth < depth) {
25
+ const childDirents = await fs.readdir(dirFullpath, { withFileTypes: true });
26
+
27
+ childDirents.sort((a, b) => {
28
+ // orders directories before files
29
+ if (a.isDirectory() && b.isFile()) {
30
+ return -1;
31
+ }
32
+
33
+ if (a.isFile() && b.isDirectory()) {
34
+ return 1;
35
+ }
36
+
37
+ // orders by name because they are the same type
38
+ // either directory & directory
39
+ // or file & file
40
+ return b.name > a.name ? -1 : 1;
41
+ });
42
+
43
+ for (let i = 0; i < childDirents.length; i++) {
44
+ const dirent = childDirents[i]!;
45
+ const isLast = i === childDirents.length - 1;
46
+
47
+ const branchingSymbol = isLast ? SYMBOLS.LAST_BRANCH : SYMBOLS.BRANCH;
48
+ const verticalSymbol = isLast ? SYMBOLS.INDENT : SYMBOLS.VERTICAL;
49
+
50
+ if (dirent.isFile()) {
51
+ lines.push(`${branchingSymbol}${dirent.name}`);
52
+ } else {
53
+ const pathToDirectory = path.join(dirFullpath, dirent.name);
54
+ const treeLinesForSubDirectory = await getTreeLines(
55
+ pathToDirectory,
56
+ depth,
57
+ currentDepth + 1,
58
+ );
59
+ lines = lines.concat(
60
+ treeLinesForSubDirectory.map((line, index) =>
61
+ index === 0
62
+ ? `${branchingSymbol}${line}`
63
+ : `${verticalSymbol}${line}`,
64
+ ),
65
+ );
66
+ }
67
+ }
68
+ }
69
+
70
+ return lines;
71
+ };
72
+
73
+ export const tree = async (dirPath: string, depth: number) => {
74
+ const lines = await getTreeLines(dirPath, depth);
75
+ return lines.join(os.EOL);
76
+ };
@@ -0,0 +1,6 @@
1
+ import type { HotReloadEvent } from './hot-reload-event.js';
2
+
3
+ export interface HotReloadChange {
4
+ filename: string;
5
+ event: HotReloadEvent;
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { EventName } from 'chokidar/handler.js';
2
+
3
+ export type HotReloadEvent = EventName;
package/tsconfig.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "display": "Next.js",
4
+ "compilerOptions": {
5
+ "composite": false,
6
+ "downlevelIteration": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "inlineSources": false,
10
+ "isolatedModules": true,
11
+ "moduleResolution": "nodenext",
12
+ "noUnusedLocals": false,
13
+ "noUnusedParameters": false,
14
+ "preserveWatchOutput": true,
15
+ "skipLibCheck": true,
16
+ "strictNullChecks": true,
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
22
+ "allowJs": true,
23
+ "declaration": false,
24
+ "declarationMap": false,
25
+ "incremental": false,
26
+ "jsx": "react-jsx",
27
+ "lib": ["dom", "dom.iterable", "esnext", "ESNext.AsyncIterable"],
28
+ "noEmit": true,
29
+ "strict": false,
30
+ "target": "ESNext",
31
+ "module": "NodeNext",
32
+ "noUncheckedIndexedAccess": true,
33
+ "resolveJsonModule": true,
34
+ "types": ["vitest/globals"],
35
+ "outDir": "dist"
36
+ },
37
+ "include": ["src/**/*.ts"],
38
+ "exclude": ["dist", "node_modules"]
39
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ dts: false,
5
+ entry: ['./src/index.ts'],
6
+ format: ['esm'],
7
+ outDir: 'dist',
8
+ });
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'happy-dom',
7
+ },
8
+ esbuild: {
9
+ tsconfigRaw: {
10
+ compilerOptions: {
11
+ jsx: 'react-jsx',
12
+ },
13
+ },
14
+ },
15
+ });