@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,151 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { getImportedModules } from './get-imported-modules.js';
3
+
4
+ vi.mock('@babel/traverse', async () => {
5
+ const traverse = await vi.importActual('@babel/traverse');
6
+ return { default: traverse };
7
+ });
8
+
9
+ describe('getImportedModules()', () => {
10
+ it('works with this test file', async () => {
11
+ const contents = await fs.readFile(import.meta.filename, 'utf8');
12
+
13
+ expect(getImportedModules(contents)).toEqual([
14
+ 'node:fs',
15
+ './get-imported-modules.js',
16
+ ]);
17
+ });
18
+
19
+ it('works with direct exports', () => {
20
+ const contents = `export * from './component-a';
21
+ export { ComponentB } from './component-b';
22
+
23
+ import { ComponentC } from './component-c';
24
+ export { ComponentC }`;
25
+ expect(getImportedModules(contents)).toEqual([
26
+ './component-a',
27
+ './component-b',
28
+ './component-c',
29
+ ]);
30
+ });
31
+
32
+ it('works with regular imports and double quotes', () => {
33
+ const contents = `import {
34
+ Body,
35
+ Button,
36
+ Container,
37
+ Column,
38
+ Head,
39
+ Heading,
40
+ Hr,
41
+ Html,
42
+ Img,
43
+ Link,
44
+ Preview,
45
+ Row,
46
+ Section,
47
+ Text,
48
+ } from "@react-email/components";
49
+ import { Tailwind } from "@react-email/tailwind";
50
+ import { Component } from '../../my-component';
51
+
52
+ import * as React from "react";
53
+ `;
54
+ expect(getImportedModules(contents)).toEqual([
55
+ '@react-email/components',
56
+ '@react-email/tailwind',
57
+ '../../my-component',
58
+ 'react',
59
+ ]);
60
+ });
61
+
62
+ it('works with regular imports and single quotes', () => {
63
+ const contents = `import {
64
+ Body,
65
+ Button,
66
+ Container,
67
+ Column,
68
+ Head,
69
+ Heading,
70
+ Hr,
71
+ Html,
72
+ Img,
73
+ Link,
74
+ Preview,
75
+ Row,
76
+ Section,
77
+ Text,
78
+ } from '@react-email/components';
79
+ import { Tailwind } from '@react-email/tailwind';
80
+ import { Component } from '../../my-component';
81
+
82
+ import * as React from 'react';
83
+ `;
84
+ expect(getImportedModules(contents)).toEqual([
85
+ '@react-email/components',
86
+ '@react-email/tailwind',
87
+ '../../my-component',
88
+ 'react',
89
+ ]);
90
+ });
91
+
92
+ it('works with commonjs require with double quotes', () => {
93
+ const contents = `const {
94
+ Body,
95
+ Button,
96
+ Container,
97
+ Column,
98
+ Head,
99
+ Heading,
100
+ Hr,
101
+ Html,
102
+ Img,
103
+ Link,
104
+ Preview,
105
+ Row,
106
+ Section,
107
+ Text,
108
+ } = require("@react-email/components");
109
+ const { Tailwind } = require("@react-email/tailwind");
110
+ const { Component } = require("../../my-component");
111
+
112
+ const React = require("react");
113
+ `;
114
+ expect(getImportedModules(contents)).toEqual([
115
+ '@react-email/components',
116
+ '@react-email/tailwind',
117
+ '../../my-component',
118
+ 'react',
119
+ ]);
120
+ });
121
+
122
+ it('works with commonjs require with single quotes', () => {
123
+ const contents = `const {
124
+ Body,
125
+ Button,
126
+ Container,
127
+ Column,
128
+ Head,
129
+ Heading,
130
+ Hr,
131
+ Html,
132
+ Img,
133
+ Link,
134
+ Preview,
135
+ Row,
136
+ Section,
137
+ Text,
138
+ } = require('@react-email/components');
139
+ const { Tailwind } = require('@react-email/tailwind');
140
+ const { Component } = require('../../my-component');
141
+
142
+ const React = require('react');
143
+ `;
144
+ expect(getImportedModules(contents)).toEqual([
145
+ '@react-email/components',
146
+ '@react-email/tailwind',
147
+ '../../my-component',
148
+ 'react',
149
+ ]);
150
+ });
151
+ });
@@ -0,0 +1,49 @@
1
+ import { parse } from '@babel/parser';
2
+
3
+ import traverseModule from '@babel/traverse';
4
+
5
+ const traverse =
6
+ // we keep this check here so that this still works with the dev:preview
7
+ // script's use of tsx
8
+ typeof traverseModule === 'function'
9
+ ? traverseModule
10
+ : traverseModule.default;
11
+
12
+ export const getImportedModules = (contents: string) => {
13
+ const importedPaths: string[] = [];
14
+ const parsedContents = parse(contents, {
15
+ sourceType: 'unambiguous',
16
+ strictMode: false,
17
+ errorRecovery: true,
18
+ plugins: ['jsx', 'typescript', 'decorators'],
19
+ });
20
+
21
+ traverse(parsedContents, {
22
+ ImportDeclaration({ node }) {
23
+ importedPaths.push(node.source.value);
24
+ },
25
+ ExportAllDeclaration({ node }) {
26
+ importedPaths.push(node.source.value);
27
+ },
28
+ ExportNamedDeclaration({ node }) {
29
+ if (node.source) {
30
+ importedPaths.push(node.source.value);
31
+ }
32
+ },
33
+ TSExternalModuleReference({ node }) {
34
+ importedPaths.push(node.expression.value);
35
+ },
36
+ CallExpression({ node }) {
37
+ if ('name' in node.callee && node.callee.name === 'require') {
38
+ if (node.arguments.length === 1) {
39
+ const importPathNode = node.arguments[0]!;
40
+ if (importPathNode!.type === 'StringLiteral') {
41
+ importedPaths.push(importPathNode.value);
42
+ }
43
+ }
44
+ }
45
+ },
46
+ });
47
+
48
+ return importedPaths;
49
+ };
@@ -0,0 +1,11 @@
1
+ import path from 'node:path';
2
+ import { resolvePathAliases } from './resolve-path-aliases.js';
3
+
4
+ test('resolveImports()', async () => {
5
+ expect(
6
+ resolvePathAliases(
7
+ ['@/some-file'],
8
+ path.resolve(import.meta.dirname, './test'),
9
+ ),
10
+ ).toEqual(['./some-file']);
11
+ });
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { createMatchPath, loadConfig } from 'tsconfig-paths';
3
+
4
+ export const resolvePathAliases = (
5
+ importPaths: string[],
6
+ projectPath: string,
7
+ ) => {
8
+ const configLoadResult = loadConfig(projectPath);
9
+
10
+ if (configLoadResult.resultType === 'success') {
11
+ const matchPath = createMatchPath(
12
+ configLoadResult.absoluteBaseUrl,
13
+ configLoadResult.paths,
14
+ );
15
+ return importPaths.map((importedPath) => {
16
+ const unaliasedPath = matchPath(importedPath, undefined, undefined, [
17
+ '.tsx',
18
+ '.ts',
19
+ '.js',
20
+ '.jsx',
21
+ '.cjs',
22
+ '.mjs',
23
+ ]);
24
+ if (unaliasedPath) {
25
+ return `./${path.relative(projectPath, unaliasedPath)}`;
26
+ }
27
+ return importedPath;
28
+ });
29
+ }
30
+
31
+ return importPaths;
32
+ };
@@ -0,0 +1,121 @@
1
+ import type http from 'node:http';
2
+ import path from 'node:path';
3
+ import { watch } from 'chokidar';
4
+ import debounce from 'debounce';
5
+ import { type Socket, Server as SocketServer } from 'socket.io';
6
+ import type { HotReloadChange } from '../../types/hot-reload-change.js';
7
+ import { createDependencyGraph } from './create-dependency-graph.js';
8
+
9
+ export const setupHotreloading = async (
10
+ devServer: http.Server,
11
+ emailDirRelativePath: string,
12
+ ) => {
13
+ let clients: Socket[] = [];
14
+ const io = new SocketServer(devServer);
15
+
16
+ io.on('connection', (client) => {
17
+ clients.push(client);
18
+
19
+ client.on('disconnect', () => {
20
+ clients = clients.filter((item) => item !== client);
21
+ });
22
+ });
23
+
24
+ // used to keep track of all changes
25
+ // and send them at once to the preview app through the web socket
26
+ let changes = [] as HotReloadChange[];
27
+
28
+ const reload = debounce(() => {
29
+ // we detect these using the useHotreload hook on the Next app
30
+ clients.forEach((client) => {
31
+ client.emit(
32
+ 'reload',
33
+ changes.filter((change) =>
34
+ // Ensures only changes inside the emails directory are emitted
35
+ path
36
+ .resolve(absolutePathToEmailsDirectory, change.filename)
37
+ .startsWith(absolutePathToEmailsDirectory),
38
+ ),
39
+ );
40
+ });
41
+
42
+ changes = [];
43
+ }, 150);
44
+
45
+ const absolutePathToEmailsDirectory = path.resolve(
46
+ process.cwd(),
47
+ emailDirRelativePath,
48
+ );
49
+
50
+ const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] =
51
+ await createDependencyGraph(absolutePathToEmailsDirectory);
52
+
53
+ const watcher = watch('', {
54
+ ignoreInitial: true,
55
+ cwd: absolutePathToEmailsDirectory,
56
+ });
57
+
58
+ const getFilesOutsideEmailsDirectory = () =>
59
+ Object.keys(dependencyGraph).filter((p) =>
60
+ path.relative(absolutePathToEmailsDirectory, p).startsWith('..'),
61
+ );
62
+ let filesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
63
+ // adds in to be watched separately all of the files that are outside of
64
+ // the user's emails directory
65
+ for (const p of filesOutsideEmailsDirectory) {
66
+ watcher.add(p);
67
+ }
68
+
69
+ const exit = async () => {
70
+ await watcher.close();
71
+ };
72
+ process.on('SIGINT', exit);
73
+ process.on('uncaughtException', exit);
74
+
75
+ watcher.on('all', async (event, relativePathToChangeTarget) => {
76
+ const file = relativePathToChangeTarget.split(path.sep);
77
+ if (file.length === 0) {
78
+ return;
79
+ }
80
+ const pathToChangeTarget = path.resolve(
81
+ absolutePathToEmailsDirectory,
82
+ relativePathToChangeTarget,
83
+ );
84
+
85
+ await updateDependencyGraph(event, pathToChangeTarget);
86
+
87
+ const newFilesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
88
+ // updates the files outside of the user's emails directory by unwatching
89
+ // the inexistent ones and watching the new ones
90
+ //
91
+ // this is necessary to avoid the issue mentioned here https://github.com/resend/react-email/issues/1433#issuecomment-2177515290
92
+ for (const p of filesOutsideEmailsDirectory) {
93
+ if (!newFilesOutsideEmailsDirectory.includes(p)) {
94
+ watcher.unwatch(p);
95
+ }
96
+ }
97
+ for (const p of newFilesOutsideEmailsDirectory) {
98
+ if (!filesOutsideEmailsDirectory.includes(p)) {
99
+ watcher.add(p);
100
+ }
101
+ }
102
+ filesOutsideEmailsDirectory = newFilesOutsideEmailsDirectory;
103
+
104
+ changes.push({
105
+ event,
106
+ filename: relativePathToChangeTarget,
107
+ });
108
+
109
+ // These dependents are dependents resolved recursively, so even dependents of dependents
110
+ // will be notified of this change so that we ensure that things are updated in the preview.
111
+ for (const dependentPath of resolveDependentsOf(pathToChangeTarget)) {
112
+ changes.push({
113
+ event: 'change' as const,
114
+ filename: path.relative(absolutePathToEmailsDirectory, dependentPath),
115
+ });
116
+ }
117
+ reload();
118
+ });
119
+
120
+ return watcher;
121
+ };
@@ -0,0 +1,5 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: used in testing */
2
+ /** biome-ignore-all lint/complexity/noUselessEmptyExport: used in testing */
3
+ import * as _ from './file-b';
4
+
5
+ export {};
@@ -0,0 +1,5 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: used in testing */
2
+ /** biome-ignore-all lint/complexity/noUselessEmptyExport: used in testing */
3
+ import * as _ from './file-a';
4
+
5
+ export {};
@@ -0,0 +1,9 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: used in testing */
2
+ /** biome-ignore-all lint/complexity/noUselessEmptyExport: used in testing */
3
+
4
+ import _os from 'node:os';
5
+ import _path from 'node:path';
6
+ import * as _outer from '../outer';
7
+ import _json from './data-to-import.json';
8
+ import * as _a from './file-a';
9
+ import * as _b from './file-b';
@@ -0,0 +1,3 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: used in testing */
2
+ /** biome-ignore-all lint/complexity/noUselessEmptyExport: used in testing */
3
+ export {};
@@ -0,0 +1,5 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: used in testing */
2
+ /** biome-ignore-all lint/complexity/noUselessEmptyExport: used in testing */
3
+ import * as _dependency from './inner/outer-dependency';
4
+
5
+ export {};
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@/*": ["./*"]
5
+ }
6
+ },
7
+ "include": ["**/*.ts", "**/*.tsx"]
8
+ }
@@ -0,0 +1,2 @@
1
+ export * from './hot-reloading/setup-hot-reloading.js';
2
+ export * from './start-dev-server.js';
@@ -0,0 +1,51 @@
1
+ import { existsSync, promises as fs } from 'node:fs';
2
+ import type http from 'node:http';
3
+ import path from 'node:path';
4
+ import type url from 'node:url';
5
+ import { lookup } from 'mime-types';
6
+
7
+ export const serveStaticFile = async (
8
+ res: http.ServerResponse,
9
+ parsedUrl: url.UrlWithParsedQuery,
10
+ staticDirRelativePath: string,
11
+ ) => {
12
+ const pathname = parsedUrl.pathname!.replace('/static', './static');
13
+ const ext = path.parse(pathname).ext;
14
+
15
+ const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath);
16
+ const fileAbsolutePath = path.resolve(staticBaseDir, pathname);
17
+ if (!fileAbsolutePath.startsWith(staticBaseDir)) {
18
+ res.statusCode = 403;
19
+ res.end();
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const fileHandle = await fs.open(fileAbsolutePath, 'r');
25
+
26
+ const fileData = await fs.readFile(fileHandle);
27
+
28
+ // if the file is found, set Content-type and send data
29
+ res.setHeader('Content-type', lookup(ext) || 'text/plain');
30
+ res.end(fileData);
31
+
32
+ fileHandle.close();
33
+ } catch (exception) {
34
+ if (!existsSync(fileAbsolutePath)) {
35
+ res.statusCode = 404;
36
+ res.end();
37
+ } else {
38
+ const sanitizedFilePath = fileAbsolutePath.replace(/\n|\r/g, '');
39
+ console.error(
40
+ `Could not read file at %s to be served, here's the exception:`,
41
+ sanitizedFilePath,
42
+ exception,
43
+ );
44
+
45
+ res.statusCode = 500;
46
+ res.end(
47
+ 'Could not read file to be served! Check your terminal for more information.',
48
+ );
49
+ }
50
+ }
51
+ };