react-email 4.1.0-canary.7 → 4.1.0-canary.9
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 +24 -0
- package/dist/{cli/index.mjs → index.js} +445 -422
- package/package.json +11 -46
- package/src/commands/build.ts +306 -0
- package/src/commands/dev.ts +27 -0
- package/src/commands/export.ts +204 -0
- package/src/commands/start.ts +38 -0
- package/src/index.ts +55 -0
- package/src/utils/__snapshots__/tree.spec.ts.snap +27 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +1 -1
- package/src/utils/get-emails-directory-metadata.spec.ts +1 -1
- package/src/utils/get-preview-server-location.ts +51 -0
- package/src/utils/index.ts +2 -6
- package/src/utils/packageJson.ts +4 -0
- package/src/utils/preview/get-env-variables-for-preview-app.ts +14 -0
- package/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts +281 -0
- package/src/utils/preview/hot-reloading/create-dependency-graph.ts +321 -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/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 +234 -0
- package/src/utils/tree.spec.ts +5 -0
- package/src/utils/tree.ts +76 -0
- package/src/utils/types/hot-reload-change.ts +1 -1
- package/src/utils/types/hot-reload-event.ts +1 -1
- package/tsconfig.json +4 -10
- package/dist/preview/.next/BUILD_ID +0 -1
- package/dist/preview/.next/app-build-manifest.json +0 -44
- package/dist/preview/.next/app-path-routes-manifest.json +0 -6
- package/dist/preview/.next/build-manifest.json +0 -33
- package/dist/preview/.next/diagnostics/build-diagnostics.json +0 -6
- package/dist/preview/.next/diagnostics/framework.json +0 -1
- package/dist/preview/.next/export-marker.json +0 -6
- package/dist/preview/.next/images-manifest.json +0 -57
- package/dist/preview/.next/next-minimal-server.js.nft.json +0 -1
- package/dist/preview/.next/next-server.js.nft.json +0 -1
- package/dist/preview/.next/package.json +0 -1
- package/dist/preview/.next/prerender-manifest.json +0 -41
- package/dist/preview/.next/react-loadable-manifest.json +0 -1
- package/dist/preview/.next/required-server-files.json +0 -311
- package/dist/preview/.next/routes-manifest.json +0 -64
- package/dist/preview/.next/server/app/_not-found/page.js +0 -1
- package/dist/preview/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +0 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/dist/preview/.next/server/app/favicon.ico.body +0 -0
- package/dist/preview/.next/server/app/favicon.ico.meta +0 -1
- package/dist/preview/.next/server/app/page.js +0 -1
- package/dist/preview/.next/server/app/page.js.nft.json +0 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +0 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +0 -321
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +0 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +0 -1
- package/dist/preview/.next/server/app-paths-manifest.json +0 -6
- package/dist/preview/.next/server/chunks/134.js +0 -6
- package/dist/preview/.next/server/chunks/235.js +0 -15
- package/dist/preview/.next/server/chunks/343.js +0 -20
- package/dist/preview/.next/server/chunks/425.js +0 -1
- package/dist/preview/.next/server/chunks/428.js +0 -14
- package/dist/preview/.next/server/chunks/963.js +0 -1
- package/dist/preview/.next/server/functions-config-manifest.json +0 -4
- package/dist/preview/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/dist/preview/.next/server/middleware-build-manifest.js +0 -1
- package/dist/preview/.next/server/middleware-manifest.json +0 -6
- package/dist/preview/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/dist/preview/.next/server/next-font-manifest.js +0 -1
- package/dist/preview/.next/server/next-font-manifest.json +0 -1
- package/dist/preview/.next/server/pages/500.html +0 -1
- package/dist/preview/.next/server/pages/_app.js +0 -1
- package/dist/preview/.next/server/pages/_app.js.nft.json +0 -1
- package/dist/preview/.next/server/pages/_document.js +0 -1
- package/dist/preview/.next/server/pages/_document.js.nft.json +0 -1
- package/dist/preview/.next/server/pages/_error.js +0 -1
- package/dist/preview/.next/server/pages/_error.js.nft.json +0 -1
- package/dist/preview/.next/server/pages-manifest.json +0 -5
- package/dist/preview/.next/server/server-reference-manifest.js +0 -1
- package/dist/preview/.next/server/server-reference-manifest.json +0 -1
- package/dist/preview/.next/server/webpack-runtime.js +0 -1
- package/dist/preview/.next/static/4K22R8mt8Z5akBgUuivvR/_buildManifest.js +0 -1
- package/dist/preview/.next/static/4K22R8mt8Z5akBgUuivvR/_ssgManifest.js +0 -1
- package/dist/preview/.next/static/chunks/107-3043079e7cb8bcae.js +0 -1
- package/dist/preview/.next/static/chunks/293-297b1eb2241f9a70.js +0 -1
- package/dist/preview/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +0 -1
- package/dist/preview/.next/static/chunks/45-1021fac82f766268.js +0 -1
- package/dist/preview/.next/static/chunks/484-25cf313c25750c6a.js +0 -1
- package/dist/preview/.next/static/chunks/589-817d8691661d370e.js +0 -1
- package/dist/preview/.next/static/chunks/902-c34acb56733e0ce1.js +0 -1
- package/dist/preview/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-ce14b7ba365bfddc.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-65fd67d48528e2ba.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-35fab824504104aa.js +0 -1
- package/dist/preview/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +0 -6
- package/dist/preview/.next/static/chunks/framework-b887e9fc751a9906.js +0 -1
- package/dist/preview/.next/static/chunks/main-9a03e7ba8acb1900.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-dbd8e1ec12eabb66.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +0 -1
- package/dist/preview/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-31c45daa2bd82a7b.js +0 -1
- package/dist/preview/.next/static/css/6f42d128f111d7fa.css +0 -3
- package/dist/preview/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
- package/dist/preview/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/dist/preview/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
- package/dist/preview/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
- package/dist/preview/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
- package/dist/preview/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
- package/dist/preview/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
- package/dist/preview/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/dist/preview/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/dist/preview/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/dist/preview/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/dist/preview/.next/static/media/logo.2ce2a759.png +0 -0
- package/dist/preview/.next/trace +0 -28
- package/dist/preview/.next/types/app/layout.ts +0 -84
- package/dist/preview/.next/types/app/page.ts +0 -84
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +0 -84
- package/dist/preview/.next/types/cache-life.d.ts +0 -141
- package/dist/preview/.next/types/package.json +0 -1
- package/module-punycode.d.ts +0 -3
- package/next-env.d.ts +0 -5
- package/next.config.js +0 -22
- package/postcss.config.js +0 -8
- package/scripts/build-preview-server.mjs +0 -33
- package/scripts/fill-caniemail-data.mjs +0 -36
- package/src/actions/email-validation/caniemail-data.ts +0 -85993
- package/src/actions/email-validation/check-compatibility.ts +0 -333
- package/src/actions/email-validation/check-images.spec.tsx +0 -100
- package/src/actions/email-validation/check-images.ts +0 -160
- package/src/actions/email-validation/check-links.spec.tsx +0 -113
- package/src/actions/email-validation/check-links.ts +0 -113
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +0 -18
- package/src/actions/email-validation/quick-fetch.ts +0 -14
- package/src/actions/get-email-path-from-slug.ts +0 -32
- package/src/actions/get-emails-directory-metadata-action.ts +0 -19
- package/src/actions/render-email-by-path.tsx +0 -121
- package/src/animated-icons-data/help.json +0 -1082
- package/src/animated-icons-data/link.json +0 -1309
- package/src/animated-icons-data/load.json +0 -443
- package/src/animated-icons-data/mail.json +0 -1320
- package/src/app/env.ts +0 -15
- package/src/app/favicon.ico +0 -0
- package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
- package/src/app/fonts.ts +0 -39
- package/src/app/globals.css +0 -15
- package/src/app/layout.tsx +0 -45
- package/src/app/logo.png +0 -0
- package/src/app/page.tsx +0 -46
- package/src/app/preview/[...slug]/page.tsx +0 -157
- package/src/app/preview/[...slug]/preview.tsx +0 -234
- package/src/app/preview/[...slug]/rendering-error.tsx +0 -40
- package/src/components/button.tsx +0 -101
- package/src/components/code-container.tsx +0 -164
- package/src/components/code-snippet.tsx +0 -9
- package/src/components/code.tsx +0 -184
- package/src/components/heading.tsx +0 -113
- package/src/components/icons/icon-arrow-down.tsx +0 -16
- package/src/components/icons/icon-base.tsx +0 -26
- package/src/components/icons/icon-bug.tsx +0 -19
- package/src/components/icons/icon-button.tsx +0 -23
- package/src/components/icons/icon-check.tsx +0 -19
- package/src/components/icons/icon-clipboard.tsx +0 -40
- package/src/components/icons/icon-download.tsx +0 -19
- package/src/components/icons/icon-email.tsx +0 -18
- package/src/components/icons/icon-file.tsx +0 -19
- package/src/components/icons/icon-folder-open.tsx +0 -19
- package/src/components/icons/icon-folder.tsx +0 -18
- package/src/components/icons/icon-hide-sidebar.tsx +0 -23
- package/src/components/icons/icon-image.tsx +0 -19
- package/src/components/icons/icon-info.tsx +0 -18
- package/src/components/icons/icon-link.tsx +0 -14
- package/src/components/icons/icon-monitor.tsx +0 -19
- package/src/components/icons/icon-moon.tsx +0 -16
- package/src/components/icons/icon-phone.tsx +0 -26
- package/src/components/icons/icon-reload.tsx +0 -18
- package/src/components/icons/icon-source.tsx +0 -19
- package/src/components/icons/icon-stamp.tsx +0 -14
- package/src/components/icons/icon-sun.tsx +0 -16
- package/src/components/icons/icon-warning.tsx +0 -31
- package/src/components/index.ts +0 -7
- package/src/components/logo.tsx +0 -63
- package/src/components/resizable-wrapper.tsx +0 -173
- package/src/components/send.tsx +0 -134
- package/src/components/shell.tsx +0 -92
- package/src/components/sidebar/file-tree-directory-children.tsx +0 -139
- package/src/components/sidebar/file-tree-directory.tsx +0 -92
- package/src/components/sidebar/file-tree.tsx +0 -31
- package/src/components/sidebar/index.ts +0 -1
- package/src/components/sidebar/sidebar.tsx +0 -43
- package/src/components/text.tsx +0 -99
- package/src/components/toolbar/checking-results.tsx +0 -150
- package/src/components/toolbar/code-preview-line-link.tsx +0 -40
- package/src/components/toolbar/compatibility.tsx +0 -113
- package/src/components/toolbar/linter.tsx +0 -278
- package/src/components/toolbar/results.tsx +0 -51
- package/src/components/toolbar/spam-assassin.tsx +0 -155
- package/src/components/toolbar/toolbar-button.tsx +0 -52
- package/src/components/toolbar/use-cached-state.ts +0 -33
- package/src/components/toolbar.tsx +0 -349
- package/src/components/tooltip-content.tsx +0 -31
- package/src/components/tooltip.tsx +0 -19
- package/src/components/topbar/active-view-toggle-group.tsx +0 -86
- package/src/components/topbar/theme-toggle-group.tsx +0 -87
- package/src/components/topbar/view-size-controls.tsx +0 -247
- package/src/components/topbar.tsx +0 -59
- package/src/contexts/emails.tsx +0 -59
- package/src/contexts/fragment-identifier.tsx +0 -48
- package/src/contexts/preview.tsx +0 -79
- package/src/hooks/use-clamped-state.ts +0 -24
- package/src/hooks/use-email-rendering-result.ts +0 -58
- package/src/hooks/use-fragment-identifier.ts +0 -14
- package/src/hooks/use-hot-reload.ts +0 -31
- package/src/hooks/use-icon-animation.ts +0 -41
- package/src/hooks/use-iframe-color-scheme.ts +0 -35
- package/src/hooks/use-rendering-metadata.ts +0 -36
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +0 -3
- package/src/utils/caniemail/all-css-properties.ts +0 -358
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +0 -74
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +0 -24
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +0 -19
- package/src/utils/caniemail/ast/get-object-variables.ts +0 -61
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +0 -23
- package/src/utils/caniemail/ast/get-used-style-properties.ts +0 -91
- package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +0 -118
- package/src/utils/caniemail/get-css-functions.ts +0 -25
- package/src/utils/caniemail/get-css-property-names.ts +0 -32
- package/src/utils/caniemail/get-css-property-with-value.ts +0 -14
- package/src/utils/caniemail/get-css-unit.ts +0 -3
- package/src/utils/caniemail/get-element-attributes.ts +0 -7
- package/src/utils/caniemail/get-element-names.ts +0 -20
- package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +0 -30
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -187
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +0 -25
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +0 -45
- package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +0 -15
- package/src/utils/cn.ts +0 -6
- package/src/utils/constants.ts +0 -6
- package/src/utils/contains-email-template.spec.ts +0 -107
- package/src/utils/contains-email-template.ts +0 -33
- package/src/utils/copy-text-to-clipboard.ts +0 -7
- package/src/utils/get-email-component.spec.ts +0 -41
- package/src/utils/get-email-component.ts +0 -134
- package/src/utils/get-line-and-column-from-offset.spec.ts +0 -11
- package/src/utils/get-line-and-column-from-offset.ts +0 -11
- package/src/utils/improve-error-with-sourcemap.ts +0 -85
- package/src/utils/js-email-detection.spec.ts +0 -24
- package/src/utils/language-map.ts +0 -7
- package/src/utils/linting.ts +0 -60
- package/src/utils/load-stream.ts +0 -15
- package/src/utils/result.ts +0 -49
- package/src/utils/run-bundled-code.ts +0 -64
- package/src/utils/sanitize.ts +0 -6
- package/src/utils/static-node-modules-for-vm.ts +0 -93
- package/src/utils/testing/js-email-export-default.js +0 -17
- package/src/utils/testing/js-email-test.js +0 -18
- package/src/utils/testing/mdx-email-test.js +0 -128
- package/src/utils/testing/request-response-email.tsx +0 -9
- package/src/utils/types/as.ts +0 -26
- package/src/utils/types/email-template.ts +0 -8
- package/src/utils/types/error-object.ts +0 -11
- package/src/utils/unreachable.ts +0 -8
- package/tailwind-internals.d.ts +0 -133
- package/tailwind.config.ts +0 -99
- /package/src/{components/toolbar/results-table.tsx → utils/preview/hot-reloading/test/some-file.ts} +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { existsSync, promises as fs, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { EventName } from 'chokidar/handler.js';
|
|
4
|
+
import { isDev } from '../start-dev-server.js';
|
|
5
|
+
import { getImportedModules } from './get-imported-modules.js';
|
|
6
|
+
import { resolvePathAliases } from './resolve-path-aliases.js';
|
|
7
|
+
|
|
8
|
+
interface Module {
|
|
9
|
+
path: string;
|
|
10
|
+
|
|
11
|
+
dependencyPaths: string[];
|
|
12
|
+
dependentPaths: string[];
|
|
13
|
+
|
|
14
|
+
moduleDependencies: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DependencyGraph = Record</* path to module */ string, Module>;
|
|
18
|
+
|
|
19
|
+
const readAllFilesInsideDirectory = async (directory: string) => {
|
|
20
|
+
let allFilePaths: string[] = [];
|
|
21
|
+
|
|
22
|
+
const topLevelDirents = await fs.readdir(directory, { withFileTypes: true });
|
|
23
|
+
|
|
24
|
+
for await (const dirent of topLevelDirents) {
|
|
25
|
+
const pathToDirent = path.join(directory, dirent.name);
|
|
26
|
+
if (dirent.isDirectory()) {
|
|
27
|
+
allFilePaths = allFilePaths.concat(
|
|
28
|
+
await readAllFilesInsideDirectory(pathToDirent),
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
allFilePaths.push(pathToDirent);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return allFilePaths;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isJavascriptModule = (filePath: string) => {
|
|
39
|
+
const extensionName = path.extname(filePath);
|
|
40
|
+
|
|
41
|
+
return ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'].includes(extensionName);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const checkFileExtensionsUntilItExists = (
|
|
45
|
+
pathWithoutExtension: string,
|
|
46
|
+
): string | undefined => {
|
|
47
|
+
if (existsSync(`${pathWithoutExtension}.ts`)) {
|
|
48
|
+
return `${pathWithoutExtension}.ts`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (existsSync(`${pathWithoutExtension}.tsx`)) {
|
|
52
|
+
return `${pathWithoutExtension}.tsx`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (existsSync(`${pathWithoutExtension}.js`)) {
|
|
56
|
+
return `${pathWithoutExtension}.js`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (existsSync(`${pathWithoutExtension}.jsx`)) {
|
|
60
|
+
return `${pathWithoutExtension}.jsx`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (existsSync(`${pathWithoutExtension}.mjs`)) {
|
|
64
|
+
return `${pathWithoutExtension}.mjs`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (existsSync(`${pathWithoutExtension}.cjs`)) {
|
|
68
|
+
return `${pathWithoutExtension}.cjs`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a stateful dependency graph that is structured in a way that you can get
|
|
74
|
+
* the dependents of a module from its path.
|
|
75
|
+
*
|
|
76
|
+
* Stateful in the sense that it provides a `getter` and an "`updater`". The updater
|
|
77
|
+
* will receive changes to the files, that can be perceived through some file watching mechanism,
|
|
78
|
+
* so that it doesn't need to recompute the entire dependency graph but only the parts changed.
|
|
79
|
+
*/
|
|
80
|
+
export const createDependencyGraph = async (directory: string) => {
|
|
81
|
+
const filePaths = await readAllFilesInsideDirectory(directory);
|
|
82
|
+
const modulePaths = filePaths.filter(isJavascriptModule);
|
|
83
|
+
const graph: DependencyGraph = Object.fromEntries(
|
|
84
|
+
modulePaths.map((path) => [
|
|
85
|
+
path,
|
|
86
|
+
{
|
|
87
|
+
path,
|
|
88
|
+
dependencyPaths: [],
|
|
89
|
+
dependentPaths: [],
|
|
90
|
+
moduleDependencies: [],
|
|
91
|
+
},
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const getDependencyPaths = async (filePath: string) => {
|
|
96
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
97
|
+
const importedPaths = isJavascriptModule(filePath)
|
|
98
|
+
? resolvePathAliases(getImportedModules(contents), path.dirname(filePath))
|
|
99
|
+
: [];
|
|
100
|
+
const importedPathsRelativeToDirectory = importedPaths.map(
|
|
101
|
+
(dependencyPath) => {
|
|
102
|
+
const isModulePath = !dependencyPath.startsWith('.');
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
path.isAbsolute will return false if the path looks like JavaScript module imports
|
|
106
|
+
e.g. path.isAbsolute('react-dom/server') will return false, but for our purposes this
|
|
107
|
+
path is not a relative one.
|
|
108
|
+
*/
|
|
109
|
+
if (isModulePath || path.isAbsolute(dependencyPath)) {
|
|
110
|
+
return dependencyPath;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let pathToDependencyFromDirectory = path.resolve(
|
|
114
|
+
/*
|
|
115
|
+
path.resolve resolves paths differently from what imports on javascript do.
|
|
116
|
+
|
|
117
|
+
So if we wouldn't do this, for an email at "/path/to/email.tsx" with a dependency path of "./other-email"
|
|
118
|
+
would end up going into /path/to/email.tsx/other-email instead of /path/to/other-email which is the
|
|
119
|
+
one the import is meant to go to
|
|
120
|
+
*/
|
|
121
|
+
path.dirname(filePath),
|
|
122
|
+
dependencyPath,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
let isDirectory = false;
|
|
126
|
+
try {
|
|
127
|
+
// will throw if the the file is not existent
|
|
128
|
+
isDirectory = statSync(pathToDependencyFromDirectory).isDirectory();
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
if (isDirectory) {
|
|
131
|
+
const pathToSubDirectory = pathToDependencyFromDirectory;
|
|
132
|
+
const pathWithExtension = checkFileExtensionsUntilItExists(
|
|
133
|
+
`${pathToSubDirectory}/index`,
|
|
134
|
+
);
|
|
135
|
+
if (pathWithExtension) {
|
|
136
|
+
pathToDependencyFromDirectory = pathWithExtension;
|
|
137
|
+
} else if (isDev) {
|
|
138
|
+
// only warn about this on development as it is probably going to be irrelevant otherwise
|
|
139
|
+
console.warn(
|
|
140
|
+
`Could not find index file for directory at ${pathToDependencyFromDirectory}. This is probably going to cause issues with both hot reloading and your code.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const extension = path.extname(pathToDependencyFromDirectory);
|
|
146
|
+
const pathWithEnsuredExtension = (() => {
|
|
147
|
+
if (
|
|
148
|
+
extension.length > 0 &&
|
|
149
|
+
existsSync(pathToDependencyFromDirectory)
|
|
150
|
+
) {
|
|
151
|
+
return pathToDependencyFromDirectory;
|
|
152
|
+
}
|
|
153
|
+
return checkFileExtensionsUntilItExists(
|
|
154
|
+
pathToDependencyFromDirectory.replace(extension, ''),
|
|
155
|
+
);
|
|
156
|
+
})();
|
|
157
|
+
|
|
158
|
+
if (pathWithEnsuredExtension) {
|
|
159
|
+
pathToDependencyFromDirectory = pathWithEnsuredExtension;
|
|
160
|
+
} else if (isDev) {
|
|
161
|
+
// only warn about this on development as it is probably going to be irrelevant otherwise
|
|
162
|
+
console.warn(
|
|
163
|
+
`Could not find file at ${pathToDependencyFromDirectory}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return pathToDependencyFromDirectory;
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const moduleDependencies = importedPathsRelativeToDirectory.filter(
|
|
172
|
+
(dependencyPath) =>
|
|
173
|
+
!dependencyPath.startsWith('.') && !path.isAbsolute(dependencyPath),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const nonNodeModuleImportPathsRelativeToDirectory =
|
|
177
|
+
importedPathsRelativeToDirectory.filter(
|
|
178
|
+
(dependencyPath) =>
|
|
179
|
+
dependencyPath.startsWith('.') || path.isAbsolute(dependencyPath),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
dependencyPaths: nonNodeModuleImportPathsRelativeToDirectory,
|
|
184
|
+
moduleDependencies,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const updateModuleDependenciesInGraph = async (moduleFilePath: string) => {
|
|
189
|
+
if (graph[moduleFilePath] === undefined) {
|
|
190
|
+
graph[moduleFilePath] = {
|
|
191
|
+
path: moduleFilePath,
|
|
192
|
+
dependencyPaths: [],
|
|
193
|
+
dependentPaths: [],
|
|
194
|
+
moduleDependencies: [],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { moduleDependencies, dependencyPaths: newDependencyPaths } =
|
|
199
|
+
await getDependencyPaths(moduleFilePath);
|
|
200
|
+
|
|
201
|
+
graph[moduleFilePath].moduleDependencies = moduleDependencies;
|
|
202
|
+
|
|
203
|
+
// we go through these to remove the ones that don't exist anymore
|
|
204
|
+
for (const dependencyPath of graph[moduleFilePath].dependencyPaths) {
|
|
205
|
+
// Looping through only the ones that were on the dependencyPaths but are not
|
|
206
|
+
// in the newDependencyPaths
|
|
207
|
+
if (newDependencyPaths.includes(dependencyPath)) continue;
|
|
208
|
+
|
|
209
|
+
const dependencyModule = graph[dependencyPath];
|
|
210
|
+
if (dependencyModule !== undefined) {
|
|
211
|
+
dependencyModule.dependentPaths =
|
|
212
|
+
dependencyModule.dependentPaths.filter(
|
|
213
|
+
(dependentPath) => dependentPath !== moduleFilePath,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
graph[moduleFilePath].dependencyPaths = newDependencyPaths;
|
|
219
|
+
|
|
220
|
+
for await (const dependencyPath of newDependencyPaths) {
|
|
221
|
+
if (graph[dependencyPath] === undefined) {
|
|
222
|
+
/*
|
|
223
|
+
This import path might have not been initialized as it can be outside
|
|
224
|
+
of the original directory we looked into.
|
|
225
|
+
*/
|
|
226
|
+
await updateModuleDependenciesInGraph(dependencyPath);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const dependencyModule = graph[dependencyPath];
|
|
230
|
+
|
|
231
|
+
if (dependencyModule === undefined) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Loading the dependency path ${dependencyPath} did not initialize it at all. This is a bug in React Email.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!dependencyModule.dependentPaths.includes(moduleFilePath)) {
|
|
238
|
+
dependencyModule.dependentPaths.push(moduleFilePath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
for (const filePath of modulePaths) {
|
|
244
|
+
await updateModuleDependenciesInGraph(filePath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const removeModuleFromGraph = (filePath: string) => {
|
|
248
|
+
const module = graph[filePath];
|
|
249
|
+
if (module) {
|
|
250
|
+
for (const dependencyPath of module.dependencyPaths) {
|
|
251
|
+
if (graph[dependencyPath]) {
|
|
252
|
+
graph[dependencyPath]!.dependentPaths = graph[
|
|
253
|
+
dependencyPath
|
|
254
|
+
]!.dependentPaths.filter(
|
|
255
|
+
(dependentPath) => dependentPath !== filePath,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
delete graph[filePath];
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return [
|
|
264
|
+
graph,
|
|
265
|
+
async (event: EventName, pathToModified: string) => {
|
|
266
|
+
switch (event) {
|
|
267
|
+
case 'change':
|
|
268
|
+
if (isJavascriptModule(pathToModified)) {
|
|
269
|
+
await updateModuleDependenciesInGraph(pathToModified);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
case 'add':
|
|
273
|
+
if (isJavascriptModule(pathToModified)) {
|
|
274
|
+
await updateModuleDependenciesInGraph(pathToModified);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
case 'addDir': {
|
|
278
|
+
const filesInsideAddedDirectory =
|
|
279
|
+
await readAllFilesInsideDirectory(pathToModified);
|
|
280
|
+
const modulesInsideAddedDirectory =
|
|
281
|
+
filesInsideAddedDirectory.filter(isJavascriptModule);
|
|
282
|
+
for await (const filePath of modulesInsideAddedDirectory) {
|
|
283
|
+
await updateModuleDependenciesInGraph(filePath);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'unlink':
|
|
288
|
+
if (isJavascriptModule(pathToModified)) {
|
|
289
|
+
removeModuleFromGraph(pathToModified);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 'unlinkDir': {
|
|
293
|
+
const filesInsideDeletedDirectory =
|
|
294
|
+
await readAllFilesInsideDirectory(pathToModified);
|
|
295
|
+
const modulesInsideDeletedDirectory =
|
|
296
|
+
filesInsideDeletedDirectory.filter(isJavascriptModule);
|
|
297
|
+
for await (const filePath of modulesInsideDeletedDirectory) {
|
|
298
|
+
removeModuleFromGraph(filePath);
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
resolveDependentsOf: function resolveDependentsOf(pathToModule: string) {
|
|
306
|
+
const moduleEntry = graph[pathToModule];
|
|
307
|
+
const dependentPaths: Array<string> = [];
|
|
308
|
+
|
|
309
|
+
if (moduleEntry) {
|
|
310
|
+
for (const dependentPath of moduleEntry.dependentPaths) {
|
|
311
|
+
const dependentsOfDependent = resolveDependentsOf(dependentPath);
|
|
312
|
+
dependentPaths.push(...dependentsOfDependent);
|
|
313
|
+
dependentPaths.push(dependentPath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return dependentPaths;
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
] as const;
|
|
321
|
+
};
|
|
@@ -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('should work 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('should work 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('should work 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('should work 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('should work 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('should work 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
|
+
};
|