react-email 4.2.6 → 4.2.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # react-email
2
2
 
3
+ ## 4.2.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 64cd6ec: fix hot reloading with circular dependencies
8
+
9
+ ## 4.2.7
10
+
3
11
  ## 4.2.6
4
12
 
5
13
  ## 4.2.5
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ import prompts from "prompts";
106
106
  // package.json
107
107
  var package_default = {
108
108
  name: "react-email",
109
- version: "4.2.6",
109
+ version: "4.2.8",
110
110
  description: "A live preview of your emails right in your browser.",
111
111
  bin: {
112
112
  email: "./dist/index.js"
@@ -758,17 +758,27 @@ var createDependencyGraph = async (directory) => {
758
758
  }
759
759
  },
760
760
  {
761
+ /**
762
+ * Resolves all modules that depend on the specified module, directly or indirectly.
763
+ *
764
+ * @param pathToModule - The path to the module whose dependents we want to find
765
+ * @returns An array of paths to all modules that depend on the specified module
766
+ */
761
767
  resolveDependentsOf: function resolveDependentsOf(pathToModule) {
762
- const moduleEntry = graph[pathToModule];
763
- const dependentPaths = [];
764
- if (moduleEntry) {
768
+ const dependentPaths = /* @__PURE__ */ new Set();
769
+ const stack = [pathToModule];
770
+ while (stack.length > 0) {
771
+ const currentPath = stack.pop();
772
+ const moduleEntry = graph[currentPath];
773
+ if (!moduleEntry) continue;
765
774
  for (const dependentPath of moduleEntry.dependentPaths) {
766
- const dependentsOfDependent = resolveDependentsOf(dependentPath);
767
- dependentPaths.push(...dependentsOfDependent);
768
- dependentPaths.push(dependentPath);
775
+ if (dependentPaths.has(dependentPath) || dependentPath === pathToModule)
776
+ continue;
777
+ dependentPaths.add(dependentPath);
778
+ stack.push(dependentPath);
769
779
  }
770
780
  }
771
- return dependentPaths;
781
+ return [...dependentPaths.values()];
772
782
  }
773
783
  }
774
784
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.2.6",
3
+ "version": "4.2.8",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/index.js"
@@ -0,0 +1,68 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`createDependencyGraph() > should have the right initial value for the dependency graph 1`] = `
4
+ {
5
+ "../outer.ts": {
6
+ "dependencyPaths": [
7
+ "outer-dependency.ts",
8
+ ],
9
+ "dependentPaths": [
10
+ "general-importing-file.ts",
11
+ ],
12
+ "moduleDependencies": [],
13
+ "path": "../outer.ts",
14
+ },
15
+ "data-to-import.json": {
16
+ "dependencyPaths": [],
17
+ "dependentPaths": [
18
+ "general-importing-file.ts",
19
+ ],
20
+ "moduleDependencies": [],
21
+ "path": "data-to-import.json",
22
+ },
23
+ "file-a.ts": {
24
+ "dependencyPaths": [
25
+ "file-b.ts",
26
+ ],
27
+ "dependentPaths": [
28
+ "file-b.ts",
29
+ "general-importing-file.ts",
30
+ ],
31
+ "moduleDependencies": [],
32
+ "path": "file-a.ts",
33
+ },
34
+ "file-b.ts": {
35
+ "dependencyPaths": [
36
+ "file-a.ts",
37
+ ],
38
+ "dependentPaths": [
39
+ "file-a.ts",
40
+ "general-importing-file.ts",
41
+ ],
42
+ "moduleDependencies": [],
43
+ "path": "file-b.ts",
44
+ },
45
+ "general-importing-file.ts": {
46
+ "dependencyPaths": [
47
+ "../outer.ts",
48
+ "data-to-import.json",
49
+ "file-a.ts",
50
+ "file-b.ts",
51
+ ],
52
+ "dependentPaths": [],
53
+ "moduleDependencies": [
54
+ "node:os",
55
+ "node:path",
56
+ ],
57
+ "path": "general-importing-file.ts",
58
+ },
59
+ "outer-dependency.ts": {
60
+ "dependencyPaths": [],
61
+ "dependentPaths": [
62
+ "../outer.ts",
63
+ ],
64
+ "moduleDependencies": [],
65
+ "path": "outer-dependency.ts",
66
+ },
67
+ }
68
+ `;
@@ -5,9 +5,11 @@ import {
5
5
  type DependencyGraph,
6
6
  } from './create-dependency-graph.js';
7
7
 
8
- const pathToFileForTestingDependencyGraph = path.join(
9
- __dirname,
10
- '.for-dependency-graph.ts',
8
+ const testingDiretctory = path.join(__dirname, './test/dependency-graph/inner');
9
+
10
+ const pathToTemporaryFile = path.join(
11
+ testingDiretctory,
12
+ './.temporary-file.ts',
11
13
  );
12
14
 
13
15
  vi.mock('@babel/traverse', async () => {
@@ -15,198 +17,130 @@ vi.mock('@babel/traverse', async () => {
15
17
  return { default: traverse };
16
18
  });
17
19
 
18
- test('createDependencyGraph()', async () => {
19
- if (existsSync(pathToFileForTestingDependencyGraph)) {
20
- await fs.rm(pathToFileForTestingDependencyGraph);
20
+ describe('createDependencyGraph()', async () => {
21
+ if (existsSync(pathToTemporaryFile)) {
22
+ await fs.rm(pathToTemporaryFile);
21
23
  }
22
24
 
23
- const [dependencyGraph, updateDependencyGraph] =
24
- await createDependencyGraph(__dirname);
25
+ const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] =
26
+ await createDependencyGraph(testingDiretctory);
25
27
 
26
28
  const toAbsolute = (relativePath: string) => {
27
- return path.resolve(__dirname, relativePath);
29
+ return path.resolve(testingDiretctory, relativePath);
28
30
  };
29
31
 
30
- const convertPathsToAbsolute = (graph: DependencyGraph) => {
31
- return Object.fromEntries(
32
- Object.entries(graph).map(([relativePath, module]) => {
33
- return [
34
- toAbsolute(relativePath),
35
- {
36
- path: toAbsolute(relativePath),
37
- dependencyPaths: module.dependencyPaths.map((p) => toAbsolute(p)),
38
- dependentPaths: module.dependentPaths.map((p) => toAbsolute(p)),
39
- moduleDependencies: module.moduleDependencies,
40
- },
41
- ];
42
- }),
43
- );
44
- };
45
-
46
- const initialDependencyGraph = convertPathsToAbsolute({
47
- 'create-dependency-graph.ts': {
48
- path: 'create-dependency-graph.ts',
49
- dependencyPaths: ['get-imported-modules.ts', 'resolve-path-aliases.ts'],
50
- dependentPaths: [
51
- 'create-dependency-graph.spec.ts',
52
- 'setup-hot-reloading.ts',
53
- ],
54
- moduleDependencies: ['node:fs', 'node:path', 'chokidar/handler.js'],
55
- },
56
- 'create-dependency-graph.spec.ts': {
57
- path: 'create-dependency-graph.spec.ts',
58
- dependencyPaths: ['create-dependency-graph.ts'],
59
- dependentPaths: [],
60
- moduleDependencies: ['node:fs', 'node:path'],
61
- },
62
- './test/some-file.ts': {
63
- dependencyPaths: [],
64
- dependentPaths: [],
65
- moduleDependencies: [],
66
- path: '/home/gabriel/Projects/Resend/react-email/packages/react-email/src/cli/utils/preview/hot-reloading/test/some-file.ts',
32
+ it.sequential(
33
+ 'should resolve dependents when there are circular dependencies',
34
+ async () => {
35
+ expect(resolveDependentsOf(toAbsolute('file-a.ts'))).toEqual([
36
+ toAbsolute('file-b.ts'),
37
+ toAbsolute('general-importing-file.ts'),
38
+ ]);
67
39
  },
68
- 'resolve-path-aliases.ts': {
69
- path: 'resolve-path-aliases.ts',
70
- dependentPaths: [
71
- 'create-dependency-graph.ts',
72
- 'resolve-path-aliases.spec.ts',
73
- ],
74
- dependencyPaths: [],
75
- moduleDependencies: ['node:path', 'tsconfig-paths'],
76
- },
77
- 'resolve-path-aliases.spec.ts': {
78
- path: 'resolve-path-aliases.spec.ts',
79
- dependencyPaths: ['resolve-path-aliases.ts'],
80
- dependentPaths: [],
81
- moduleDependencies: ['node:path'],
82
- },
83
- 'get-imported-modules.ts': {
84
- path: 'get-imported-modules',
85
- dependentPaths: [
86
- 'create-dependency-graph.ts',
87
- 'get-imported-modules.spec.ts',
88
- ],
89
- dependencyPaths: [],
90
- moduleDependencies: ['@babel/parser', '@babel/traverse'],
40
+ );
41
+
42
+ it.sequential(
43
+ 'should have the right initial value for the dependency graph',
44
+ () => {
45
+ const relativePathDependencyGraph = Object.fromEntries(
46
+ Object.entries(dependencyGraph).map(([key, value]) => {
47
+ return [
48
+ path.relative(testingDiretctory, key),
49
+ {
50
+ path: path.relative(testingDiretctory, value.path),
51
+ dependentPaths: value.dependentPaths.map((p) =>
52
+ path.relative(testingDiretctory, p),
53
+ ),
54
+ dependencyPaths: value.dependencyPaths.map((p) =>
55
+ path.relative(testingDiretctory, p),
56
+ ),
57
+ moduleDependencies: value.moduleDependencies,
58
+ },
59
+ ];
60
+ }),
61
+ );
62
+ expect(relativePathDependencyGraph).toMatchSnapshot();
91
63
  },
92
- 'get-imported-modules.spec.ts': {
93
- path: 'get-imported-modules.spec.ts',
94
- dependencyPaths: ['get-imported-modules.ts'],
64
+ );
65
+
66
+ it.sequential('should work when adding a new file', async () => {
67
+ await fs.writeFile(
68
+ pathToTemporaryFile,
69
+ `
70
+ import {} from './file-a';
71
+ import {} from './file-b';
72
+ import {} from './general-importing-file';
73
+ `,
74
+ 'utf8',
75
+ );
76
+ await updateDependencyGraph('add', pathToTemporaryFile);
77
+ expect(dependencyGraph[pathToTemporaryFile]).toEqual({
78
+ path: pathToTemporaryFile,
95
79
  dependentPaths: [],
96
- moduleDependencies: ['node:fs'],
97
- },
98
- 'setup-hot-reloading.ts': {
99
- path: 'setup-hot-reloading.ts',
100
80
  dependencyPaths: [
101
- '../../types/hot-reload-change.ts',
102
- 'create-dependency-graph.ts',
81
+ toAbsolute('file-a.ts'),
82
+ toAbsolute('file-b.ts'),
83
+ toAbsolute('general-importing-file.ts'),
103
84
  ],
104
- dependentPaths: [],
105
- moduleDependencies: [
106
- 'node:http',
107
- 'node:path',
108
- 'chokidar',
109
- 'debounce',
110
- 'socket.io',
111
- ],
112
- },
113
- '../../types/hot-reload-event.ts': {
114
- dependencyPaths: [],
115
- dependentPaths: ['../../types/hot-reload-change.ts'],
116
- moduleDependencies: ['chokidar/handler.js'],
117
- path: '../../types/hot-reload-event.ts',
118
- },
119
- '../../types/hot-reload-change.ts': {
120
- path: '../../types/hot-reload-change.ts',
121
- dependencyPaths: ['../../types/hot-reload-event.ts'],
122
- dependentPaths: ['setup-hot-reloading.ts'],
123
85
  moduleDependencies: [],
124
- },
125
- } satisfies DependencyGraph);
126
-
127
- expect(
128
- dependencyGraph,
129
- 'the initial value for the dependency graph should work with the directory of this testing file',
130
- ).toEqual(initialDependencyGraph);
131
-
132
- await fs.writeFile(
133
- pathToFileForTestingDependencyGraph,
134
- `
135
- import {} from './setup-hot-reloading';
136
- import {} from './get-imported-modules';
137
- import {} from './create-dependency-graph.ts';
138
- `,
139
- 'utf8',
140
- );
141
- await updateDependencyGraph('add', pathToFileForTestingDependencyGraph);
142
- expect(
143
- dependencyGraph[pathToFileForTestingDependencyGraph],
144
- 'added file to have proper dependency paths',
145
- ).toEqual({
146
- path: pathToFileForTestingDependencyGraph,
147
- dependentPaths: [],
148
- dependencyPaths: [
149
- toAbsolute('setup-hot-reloading.ts'),
150
- toAbsolute('get-imported-modules.ts'),
151
- toAbsolute('create-dependency-graph.ts'),
152
- ],
153
- moduleDependencies: [],
154
- } satisfies DependencyGraph[number]);
155
- expect(
156
- dependencyGraph[toAbsolute('setup-hot-reloading.ts')]?.dependentPaths,
157
- ).toContain(pathToFileForTestingDependencyGraph);
158
- expect(
159
- dependencyGraph[toAbsolute('get-imported-modules.ts')]?.dependentPaths,
160
- ).toContain(pathToFileForTestingDependencyGraph);
161
- expect(
162
- dependencyGraph[toAbsolute('create-dependency-graph.ts')]?.dependentPaths,
163
- ).toContain(pathToFileForTestingDependencyGraph);
86
+ } satisfies DependencyGraph[number]);
87
+ expect(dependencyGraph[toAbsolute('file-a.ts')]?.dependentPaths).toContain(
88
+ pathToTemporaryFile,
89
+ );
90
+ expect(dependencyGraph[toAbsolute('file-b.ts')]?.dependentPaths).toContain(
91
+ pathToTemporaryFile,
92
+ );
93
+ expect(
94
+ dependencyGraph[toAbsolute('general-importing-file.ts')]?.dependentPaths,
95
+ ).toContain(pathToTemporaryFile);
96
+ });
164
97
 
165
- await fs.writeFile(
166
- pathToFileForTestingDependencyGraph,
167
- `
168
- import {} from './setup-hot-reloading';
169
- import {} from './create-dependency-graph.ts';
98
+ it.sequential('should work when updating a file', async () => {
99
+ await fs.writeFile(
100
+ pathToTemporaryFile,
101
+ `
102
+ import {} from './file-a';
103
+ import {} from './file-b';
170
104
  `,
171
- 'utf8',
172
- );
173
- await updateDependencyGraph('change', pathToFileForTestingDependencyGraph);
174
- expect(
175
- dependencyGraph[pathToFileForTestingDependencyGraph],
176
- 'changed file to have updated dependencyPaths',
177
- ).toEqual({
178
- path: pathToFileForTestingDependencyGraph,
179
- dependentPaths: [],
180
- dependencyPaths: [
181
- toAbsolute('setup-hot-reloading.ts'),
182
- toAbsolute('create-dependency-graph.ts'),
183
- ],
184
- moduleDependencies: [],
185
- } satisfies DependencyGraph[number]);
186
- expect(
187
- dependencyGraph[toAbsolute('setup-hot-reloading.ts')]?.dependentPaths,
188
- ).toContain(pathToFileForTestingDependencyGraph);
189
- expect(
190
- dependencyGraph[toAbsolute('get-imported-modules.ts')]?.dependentPaths,
191
- 'when removing dependency on a file, the dependency should have its dependents updated to not have the testing file again',
192
- ).not.toContain(pathToFileForTestingDependencyGraph);
193
- expect(
194
- dependencyGraph[toAbsolute('create-dependency-graph.ts')]?.dependentPaths,
195
- ).toContain(pathToFileForTestingDependencyGraph);
105
+ 'utf8',
106
+ );
107
+ await updateDependencyGraph('change', pathToTemporaryFile);
108
+ expect(
109
+ dependencyGraph[pathToTemporaryFile],
110
+ 'changed file to have updated dependencyPaths',
111
+ ).toEqual({
112
+ path: pathToTemporaryFile,
113
+ dependentPaths: [],
114
+ dependencyPaths: [toAbsolute('file-a.ts'), toAbsolute('file-b.ts')],
115
+ moduleDependencies: [],
116
+ } satisfies DependencyGraph[number]);
117
+ expect(dependencyGraph[toAbsolute('file-a.ts')]?.dependentPaths).toContain(
118
+ pathToTemporaryFile,
119
+ );
120
+ expect(dependencyGraph[toAbsolute('file-b.ts')]?.dependentPaths).toContain(
121
+ pathToTemporaryFile,
122
+ );
123
+ expect(
124
+ dependencyGraph[toAbsolute('general-importing-file.ts')]?.dependentPaths,
125
+ 'when removing dependency on a file, the dependency should have its dependents updated to not have the testing file again',
126
+ ).not.toContain(pathToTemporaryFile);
127
+ });
196
128
 
197
- await fs.rm(pathToFileForTestingDependencyGraph);
198
- await updateDependencyGraph('unlink', pathToFileForTestingDependencyGraph);
199
- expect(dependencyGraph[pathToFileForTestingDependencyGraph]).toBeUndefined();
200
- expect(
201
- dependencyGraph[toAbsolute('setup-hot-reloading.ts')]?.dependentPaths,
202
- "should remove itself from dependents once it's unlinked",
203
- ).not.toContain(pathToFileForTestingDependencyGraph);
204
- expect(
205
- dependencyGraph[toAbsolute('get-imported-modules.ts')]?.dependentPaths,
206
- "should remove itself from dependents once it's unlinked",
207
- ).not.toContain(pathToFileForTestingDependencyGraph);
208
- expect(
209
- dependencyGraph[toAbsolute('create-dependency-graph.ts')]?.dependentPaths,
210
- "should remove itself from dependents once it's unlinked",
211
- ).not.toContain(pathToFileForTestingDependencyGraph);
129
+ it.sequential('should work when unlinking a file', async () => {
130
+ await fs.rm(pathToTemporaryFile);
131
+ await updateDependencyGraph('unlink', pathToTemporaryFile);
132
+ expect(dependencyGraph[pathToTemporaryFile]).toBeUndefined();
133
+ expect(
134
+ dependencyGraph[toAbsolute('file-a.ts')]?.dependentPaths,
135
+ "should remove itself from dependents once it's unlinked",
136
+ ).not.toContain(pathToTemporaryFile);
137
+ expect(
138
+ dependencyGraph[toAbsolute('file-b.ts')]?.dependentPaths,
139
+ "should remove itself from dependents once it's unlinked",
140
+ ).not.toContain(pathToTemporaryFile);
141
+ expect(
142
+ dependencyGraph[toAbsolute('general-importing-file.ts')]?.dependentPaths,
143
+ "should remove itself from dependents once it's unlinked",
144
+ ).not.toContain(pathToTemporaryFile);
145
+ });
212
146
  });
@@ -306,19 +306,37 @@ export const createDependencyGraph = async (directory: string) => {
306
306
  }
307
307
  },
308
308
  {
309
- resolveDependentsOf: function resolveDependentsOf(pathToModule: string) {
310
- const moduleEntry = graph[pathToModule];
311
- const dependentPaths: Array<string> = [];
309
+ /**
310
+ * Resolves all modules that depend on the specified module, directly or indirectly.
311
+ *
312
+ * @param pathToModule - The path to the module whose dependents we want to find
313
+ * @returns An array of paths to all modules that depend on the specified module
314
+ */
315
+ resolveDependentsOf: function resolveDependentsOf(
316
+ pathToModule: string,
317
+ ): string[] {
318
+ const dependentPaths = new Set<string>();
319
+ const stack: string[] = [pathToModule];
320
+
321
+ while (stack.length > 0) {
322
+ const currentPath = stack.pop()!;
323
+ const moduleEntry = graph[currentPath];
324
+
325
+ if (!moduleEntry) continue;
312
326
 
313
- if (moduleEntry) {
314
327
  for (const dependentPath of moduleEntry.dependentPaths) {
315
- const dependentsOfDependent = resolveDependentsOf(dependentPath);
316
- dependentPaths.push(...dependentsOfDependent);
317
- dependentPaths.push(dependentPath);
328
+ if (
329
+ dependentPaths.has(dependentPath) ||
330
+ dependentPath === pathToModule
331
+ )
332
+ continue;
333
+
334
+ dependentPaths.add(dependentPath);
335
+ stack.push(dependentPath);
318
336
  }
319
337
  }
320
338
 
321
- return dependentPaths;
339
+ return [...dependentPaths.values()];
322
340
  },
323
341
  },
324
342
  ] as const;
@@ -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 {};