@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.
- package/CHANGELOG.md +652 -0
- package/README.md +43 -0
- package/changes.json +58 -0
- package/dev/CHANGELOG.md +13 -0
- package/dev/index.js +46 -0
- package/dev/package.json +10 -0
- package/dist/index.mjs +7326 -0
- package/license.md +7 -0
- package/package.json +128 -0
- package/readme.md +59 -0
- package/src/commands/build.ts +269 -0
- package/src/commands/dev.ts +27 -0
- package/src/commands/export.ts +204 -0
- package/src/commands/resend/reset.ts +8 -0
- package/src/commands/resend/setup.ts +29 -0
- package/src/commands/start.ts +38 -0
- package/src/index.ts +110 -0
- package/src/utils/conf.ts +9 -0
- package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
- package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
- package/src/utils/get-emails-directory-metadata.ts +140 -0
- package/src/utils/get-preview-server-location.ts +50 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/packageJson.ts +4 -0
- package/src/utils/preview/get-env-variables-for-preview-app.ts +20 -0
- package/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts +226 -0
- package/src/utils/preview/hot-reloading/create-dependency-graph.ts +343 -0
- package/src/utils/preview/hot-reloading/get-imported-modules.spec.ts +151 -0
- package/src/utils/preview/hot-reloading/get-imported-modules.ts +49 -0
- package/src/utils/preview/hot-reloading/resolve-path-aliases.spec.ts +11 -0
- package/src/utils/preview/hot-reloading/resolve-path-aliases.ts +32 -0
- package/src/utils/preview/hot-reloading/setup-hot-reloading.ts +121 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/data-to-import.json +1 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/file-a.ts +5 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/file-b.ts +5 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/general-importing-file.ts +9 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/outer-dependency.ts +3 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/inner/path-aliases.ts +1 -0
- package/src/utils/preview/hot-reloading/test/dependency-graph/outer.ts +5 -0
- package/src/utils/preview/hot-reloading/test/some-file.ts +0 -0
- package/src/utils/preview/hot-reloading/test/tsconfig.json +8 -0
- package/src/utils/preview/index.ts +2 -0
- package/src/utils/preview/serve-static-file.ts +51 -0
- package/src/utils/preview/start-dev-server.ts +252 -0
- package/src/utils/register-spinner-autostopping.ts +28 -0
- package/src/utils/style-text.ts +11 -0
- package/src/utils/tree.spec.ts +29 -0
- package/src/utils/tree.ts +76 -0
- package/src/utils/types/hot-reload-change.ts +6 -0
- package/src/utils/types/hot-reload-event.ts +3 -0
- package/tsconfig.json +39 -0
- package/tsdown.config.ts +8 -0
- 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 @@
|
|
|
1
|
+
{}
|
|
@@ -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 @@
|
|
|
1
|
+
import '@/some-file';
|
|
File without changes
|
|
@@ -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
|
+
};
|