react-email 1.7.5 → 1.7.7

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 (52) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/source/_preview/components.js +2 -2
  3. package/dist/source/_preview/root.js +1 -1
  4. package/dist/source/commands/dev.d.ts +5 -1
  5. package/dist/source/commands/dev.js +25 -20
  6. package/dist/source/commands/export.d.ts +1 -1
  7. package/dist/source/commands/export.js +6 -4
  8. package/dist/source/index.js +6 -4
  9. package/dist/source/utils/watcher.d.ts +3 -3
  10. package/dist/source/utils/watcher.js +19 -14
  11. package/package.json +1 -1
  12. package/preview/package.json +2 -2
  13. package/preview/src/components/code-container.tsx +1 -1
  14. package/preview/src/components/code.tsx +1 -1
  15. package/source/_preview/components.ts +2 -2
  16. package/source/_preview/root.ts +1 -1
  17. package/source/commands/dev.ts +31 -32
  18. package/source/commands/export.ts +11 -10
  19. package/source/index.ts +7 -5
  20. package/source/utils/watcher.ts +25 -24
  21. package/dist/_preview/components.d.ts +0 -4
  22. package/dist/_preview/components.js +0 -77
  23. package/dist/_preview/pages.d.ts +0 -9
  24. package/dist/_preview/pages.js +0 -22
  25. package/dist/_preview/root.d.ts +0 -4
  26. package/dist/_preview/root.js +0 -29
  27. package/dist/_preview/styles.d.ts +0 -4
  28. package/dist/_preview/styles.js +0 -9
  29. package/dist/_preview/utils.d.ts +0 -4
  30. package/dist/_preview/utils.js +0 -25
  31. package/dist/commands/dev.d.ts +0 -1
  32. package/dist/commands/dev.js +0 -186
  33. package/dist/commands/export.d.ts +0 -2
  34. package/dist/commands/export.js +0 -80
  35. package/dist/commands/exportTemplates.d.ts +0 -1
  36. package/dist/commands/exportTemplates.js +0 -79
  37. package/dist/index.d.ts +0 -2
  38. package/dist/index.js +0 -23
  39. package/dist/utils/check-directory-exist.d.ts +0 -1
  40. package/dist/utils/check-directory-exist.js +0 -9
  41. package/dist/utils/check-empty-directory.d.ts +0 -1
  42. package/dist/utils/check-empty-directory.js +0 -12
  43. package/dist/utils/check-is-up-to-date.d.ts +0 -2
  44. package/dist/utils/check-is-up-to-date.js +0 -26
  45. package/dist/utils/constants.d.ts +0 -10
  46. package/dist/utils/constants.js +0 -22
  47. package/dist/utils/create-directory.d.ts +0 -1
  48. package/dist/utils/create-directory.js +0 -9
  49. package/dist/utils/index.d.ts +0 -6
  50. package/dist/utils/index.js +0 -22
  51. package/dist/utils/watcher.d.ts +0 -3
  52. package/dist/utils/watcher.js +0 -37
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/index.js"
@@ -8,11 +8,11 @@ exports.components = [
8
8
  },
9
9
  {
10
10
  title: 'code-container.tsx',
11
- content: "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [hovered, setHovered] = React.useState('');\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a\n href={url}\n download={file.name}\n className=\"text-slate-11 transition ease-in-out duration-200 hover:text-slate-12\"\n >\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"flex\">\n <AnimateSharedLayout>\n {markups.map(({ language }) => {\n const isHovered = hovered === language;\n return (\n <motion.button\n className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 ${\n activeTab !== language ? 'text-slate-11' : 'text-slate-12'\n }`}\n onClick={() => setActiveTab(language)}\n onHoverStart={() => setHovered(language)}\n onHoverEnd={() => setHovered('')}\n key={language}\n >\n {isHovered && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n {languageMap[language]}\n </motion.button>\n );\n })}\n </AnimateSharedLayout>\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
11
+ content: "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [hovered, setHovered] = React.useState('');\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a\n href={url}\n download={file.name}\n className=\"text-slate-11 transition ease-in-out duration-200 hover:text-slate-12\"\n >\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"flex\">\n <AnimateSharedLayout>\n {markups.map(({ language }) => {\n const isHovered = hovered === language;\n return (\n <motion.button\n className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 ${\n activeTab !== language ? 'text-slate-11' : 'text-slate-12'\n }`}\n onClick={() => setActiveTab(language)}\n onHoverStart={() => setHovered(language)}\n onHoverEnd={() => setHovered('')}\n key={language}\n >\n {isHovered && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n {languageMap[language]}\n </motion.button>\n );\n })}\n </AnimateSharedLayout>\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
12
12
  },
13
13
  {
14
14
  title: 'code.tsx',
15
- content: "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
15
+ content: "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4 h-[650px] overflow-auto\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
16
16
  },
17
17
  {
18
18
  title: 'heading.tsx',
@@ -12,7 +12,7 @@ exports.root = [
12
12
  },
13
13
  {
14
14
  title: 'package.json',
15
- content: '{\n "name": "react-email-preview",\n "version": "0.0.10",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.6",\n "classnames": "2.3.2",\n "framer-motion": "8.4.6",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
15
+ content: '{\n "name": "react-email-preview",\n "version": "0.0.11",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=16.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.6",\n "classnames": "2.3.2",\n "framer-motion": "8.4.6",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
16
16
  },
17
17
  {
18
18
  title: 'postcss.config.js',
@@ -1 +1,5 @@
1
- export declare const dev: () => Promise<void>;
1
+ interface Args {
2
+ dir: string;
3
+ }
4
+ export declare const dev: ({ dir }: Args) => Promise<void>;
5
+ export {};
@@ -18,7 +18,9 @@ const ora_1 = __importDefault(require("ora"));
18
18
  const read_pkg_1 = __importDefault(require("read-pkg"));
19
19
  const shelljs_1 = __importDefault(require("shelljs"));
20
20
  const styles_1 = require("../_preview/styles");
21
- const dev = async () => {
21
+ const dev = async ({ dir }) => {
22
+ const emailDir = convertToAbsolutePath(dir);
23
+ const watcherInstance = (0, utils_1.createWatcherInstance)(emailDir);
22
24
  try {
23
25
  const hasReactEmailDirectory = (0, utils_1.checkDirectoryExist)(utils_1.REACT_EMAIL_ROOT);
24
26
  let packageManager;
@@ -31,10 +33,10 @@ const dev = async () => {
31
33
  if (hasReactEmailDirectory) {
32
34
  const isUpToDate = await (0, utils_1.checkPackageIsUpToDate)();
33
35
  if (isUpToDate) {
34
- await Promise.all([generateEmailsPreview(), syncPkg()]);
36
+ await Promise.all([generateEmailsPreview(emailDir), syncPkg()]);
35
37
  await installDependencies(packageManager);
36
38
  shelljs_1.default.exec(`${packageManager} run dev`, { async: true });
37
- (0, utils_1.watcher)();
39
+ (0, utils_1.watcher)(watcherInstance, emailDir);
38
40
  return;
39
41
  }
40
42
  await fs_1.default.promises.rm(utils_1.REACT_EMAIL_ROOT, { recursive: true });
@@ -42,17 +44,18 @@ const dev = async () => {
42
44
  await createBasicStructure();
43
45
  await createAppDirectories();
44
46
  await createAppFiles();
45
- await Promise.all([generateEmailsPreview(), syncPkg()]);
47
+ await Promise.all([generateEmailsPreview(emailDir), syncPkg()]);
46
48
  await installDependencies(packageManager);
47
49
  shelljs_1.default.exec(`${packageManager} run dev`, { async: true });
48
- (0, utils_1.watcher)();
50
+ (0, utils_1.watcher)(watcherInstance, emailDir);
49
51
  }
50
52
  catch (error) {
51
- await utils_1.watcherInstance.close();
53
+ await watcherInstance.close();
52
54
  shelljs_1.default.exit(1);
53
55
  }
54
56
  };
55
57
  exports.dev = dev;
58
+ const convertToAbsolutePath = (dir) => path_1.default.isAbsolute(dir) ? dir : path_1.default.join(process.cwd(), dir);
56
59
  const createBasicStructure = async () => {
57
60
  try {
58
61
  // Create `.react-email` directory
@@ -111,12 +114,12 @@ const createAppFiles = async () => {
111
114
  throw new Error('Error creating app files');
112
115
  }
113
116
  };
114
- const generateEmailsPreview = async () => {
117
+ const generateEmailsPreview = async (emailDir) => {
115
118
  try {
116
119
  const spinner = (0, ora_1.default)('Generating emails preview').start();
117
- await createEmailPreviews();
118
- await createStatisFiles();
119
- await createComponents();
120
+ await createEmailPreviews(emailDir);
121
+ await createStaticFiles(emailDir);
122
+ await createComponents(emailDir);
120
123
  spinner.stopAndPersist({
121
124
  symbol: log_symbols_1.default.success,
122
125
  text: 'Emails preview generated',
@@ -126,10 +129,10 @@ const generateEmailsPreview = async () => {
126
129
  console.log({ error });
127
130
  }
128
131
  };
129
- const createEmailPreviews = async () => {
130
- const hasEmailsDirectory = (0, utils_1.checkDirectoryExist)(utils_1.CLIENT_EMAILS_PATH);
132
+ const createEmailPreviews = async (emailDir) => {
133
+ const hasEmailsDirectory = (0, utils_1.checkDirectoryExist)(emailDir);
131
134
  const isEmailsDirectoryEmpty = hasEmailsDirectory
132
- ? await (0, utils_1.checkEmptyDirectory)(utils_1.CLIENT_EMAILS_PATH)
135
+ ? await (0, utils_1.checkEmptyDirectory)(emailDir)
133
136
  : true;
134
137
  if (isEmailsDirectoryEmpty) {
135
138
  }
@@ -137,30 +140,32 @@ const createEmailPreviews = async () => {
137
140
  if (hasPackageEmailsDirectory) {
138
141
  await fs_1.default.promises.rm(utils_1.PACKAGE_EMAILS_PATH, { recursive: true });
139
142
  }
140
- await (0, cpy_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/*{.tsx,.jsx}`, utils_1.PACKAGE_EMAILS_PATH);
143
+ await (0, cpy_1.default)(path_1.default.join(emailDir, '*{.tsx,.jsx}'), utils_1.PACKAGE_EMAILS_PATH);
141
144
  };
142
- const createStatisFiles = async () => {
145
+ const createStaticFiles = async (emailDir) => {
143
146
  const hasPackageStaticDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.REACT_EMAIL_ROOT}/public/static`);
144
- const hasStaticDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.CLIENT_EMAILS_PATH}/static`);
147
+ const staticDir = path_1.default.join(emailDir, 'static');
148
+ const hasStaticDirectory = (0, utils_1.checkDirectoryExist)(staticDir);
145
149
  if (hasPackageStaticDirectory) {
146
150
  await fs_1.default.promises.rm(`${utils_1.REACT_EMAIL_ROOT}/public/static`, {
147
151
  recursive: true,
148
152
  });
149
153
  }
150
154
  if (hasStaticDirectory) {
151
- await (0, cpy_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/static`, `${utils_1.REACT_EMAIL_ROOT}/public/static`);
155
+ await (0, cpy_1.default)(staticDir, `${utils_1.REACT_EMAIL_ROOT}/public/static`);
152
156
  }
153
157
  };
154
- const createComponents = async () => {
158
+ const createComponents = async (emailDir) => {
155
159
  const hasPackageComponentsDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.PACKAGE_EMAILS_PATH}/components`);
156
- const hasComponentsDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.CLIENT_EMAILS_PATH}/components`);
160
+ const componentDir = path_1.default.join(emailDir, 'components');
161
+ const hasComponentsDirectory = (0, utils_1.checkDirectoryExist)(componentDir);
157
162
  if (hasPackageComponentsDirectory) {
158
163
  await fs_1.default.promises.rm(`${utils_1.PACKAGE_EMAILS_PATH}/components`, {
159
164
  recursive: true,
160
165
  });
161
166
  }
162
167
  if (hasComponentsDirectory) {
163
- await (0, cpy_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/components`, `${utils_1.PACKAGE_EMAILS_PATH}/components`);
168
+ await (0, cpy_1.default)(componentDir, `${utils_1.PACKAGE_EMAILS_PATH}/components`);
164
169
  }
165
170
  };
166
171
  const syncPkg = async () => {
@@ -1,2 +1,2 @@
1
1
  import { Options } from '@react-email/render';
2
- export declare const exportTemplates: (outDir: string, options: Options) => Promise<never>;
2
+ export declare const exportTemplates: (outDir: string, srcDir: string, options: Options) => Promise<never>;
@@ -37,14 +37,15 @@ const fs_1 = require("fs");
37
37
  const cpy_1 = __importDefault(require("cpy"));
38
38
  const normalize_path_1 = __importDefault(require("normalize-path"));
39
39
  const utils_1 = require("../utils");
40
+ const path_1 = __importDefault(require("path"));
40
41
  /*
41
42
  This first builds all the templates using esbuild and then puts the output in the `.js`
42
43
  files. Then these `.js` files are imported dynamically and rendered to `.html` files
43
44
  using the `render` function.
44
45
  */
45
- const exportTemplates = async (outDir, options) => {
46
+ const exportTemplates = async (outDir, srcDir, options) => {
46
47
  const spinner = (0, ora_1.default)('Preparing files...\n').start();
47
- const allTemplates = glob_1.glob.sync((0, normalize_path_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/*.{tsx,jsx}`));
48
+ const allTemplates = glob_1.glob.sync((0, normalize_path_1.default)(path_1.default.join(srcDir, '*.{tsx,jsx}')));
48
49
  esbuild_1.default.buildSync({
49
50
  bundle: true,
50
51
  entryPoints: allTemplates,
@@ -62,9 +63,10 @@ const exportTemplates = async (outDir, options) => {
62
63
  (0, fs_1.writeFileSync)(htmlPath, rendered);
63
64
  (0, fs_1.unlinkSync)(template);
64
65
  }
65
- const hasStaticDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.CLIENT_EMAILS_PATH}/static`);
66
+ const staticDir = path_1.default.join(srcDir, 'static');
67
+ const hasStaticDirectory = (0, utils_1.checkDirectoryExist)(staticDir);
66
68
  if (hasStaticDirectory) {
67
- await (0, cpy_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/static`, `${outDir}/static`);
69
+ await (0, cpy_1.default)(staticDir, `${outDir}/static`);
68
70
  }
69
71
  const fileTree = (0, tree_node_cli_1.default)(outDir, {
70
72
  allFiles: true,
@@ -5,9 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const extra_typings_1 = require("@commander-js/extra-typings");
8
- const constants_1 = require("./utils/constants");
9
8
  const dev_1 = require("./commands/dev");
10
9
  const export_1 = require("./commands/export");
10
+ const constants_1 = require("./utils/constants");
11
11
  const package_json_1 = __importDefault(require("../package.json"));
12
12
  extra_typings_1.program
13
13
  .name(constants_1.PACKAGE_NAME)
@@ -16,12 +16,14 @@ extra_typings_1.program
16
16
  extra_typings_1.program
17
17
  .command('dev')
18
18
  .description('Starts the application in development mode')
19
- .action(dev_1.dev);
19
+ .option('-d, --dir <path>', 'Directory with your email templates', './emails')
20
+ .action((args) => (0, dev_1.dev)(args));
20
21
  extra_typings_1.program
21
22
  .command('export')
22
23
  .description('Build the templates to the `out` directory')
23
24
  .option('--outDir <path>', 'Output directory', 'out')
24
25
  .option('-p, --pretty', 'Pretty print the output', false)
25
- .option('-t, --plainText', 'Set output format as plain Text', false)
26
- .action(({ outDir, pretty, plainText }) => (0, export_1.exportTemplates)(outDir, { pretty, plainText }));
26
+ .option('-t, --plainText', 'Set output format as plain text', false)
27
+ .option('-d, --dir <path>', 'Directory with your email templates', './emails')
28
+ .action(({ outDir, pretty, plainText, dir: srcDir }) => (0, export_1.exportTemplates)(outDir, srcDir, { pretty, plainText }));
27
29
  extra_typings_1.program.parse();
@@ -1,3 +1,3 @@
1
- import chokidar from 'chokidar';
2
- export declare const watcherInstance: chokidar.FSWatcher;
3
- export declare const watcher: () => chokidar.FSWatcher;
1
+ import chokidar, { FSWatcher } from 'chokidar';
2
+ export declare const createWatcherInstance: (watchDir: string) => chokidar.FSWatcher;
3
+ export declare const watcher: (watcherInstance: FSWatcher, watchDir: string) => void;
@@ -3,33 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.watcher = exports.watcherInstance = void 0;
6
+ exports.watcher = exports.createWatcherInstance = void 0;
7
7
  const chokidar_1 = __importDefault(require("chokidar"));
8
8
  const constants_1 = require("./constants");
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const cpy_1 = __importDefault(require("cpy"));
12
- exports.watcherInstance = chokidar_1.default.watch(constants_1.CLIENT_EMAILS_PATH, {
12
+ const createWatcherInstance = (watchDir) => chokidar_1.default.watch(watchDir, {
13
13
  ignoreInitial: true,
14
14
  cwd: constants_1.CURRENT_PATH,
15
15
  ignored: /(^|[\/\\])\../,
16
16
  });
17
- const watcher = () => exports.watcherInstance.on('all', async (event, filename) => {
18
- if (event === constants_1.EVENT_FILE_DELETED) {
17
+ exports.createWatcherInstance = createWatcherInstance;
18
+ const watcher = (watcherInstance, watchDir) => {
19
+ watcherInstance.on('all', async (event, filename) => {
19
20
  const file = filename.split(path_1.default.sep);
20
- if (file[1] === 'static' && file[2]) {
21
- await fs_1.default.promises.rm(path_1.default.join(constants_1.REACT_EMAIL_ROOT, 'public', 'static', file[2]));
21
+ if (file[1] === undefined) {
22
+ return;
23
+ }
24
+ if (event === constants_1.EVENT_FILE_DELETED) {
25
+ if (file[1] === 'static' && file[2]) {
26
+ await fs_1.default.promises.rm(path_1.default.join(constants_1.REACT_EMAIL_ROOT, 'public', 'static', file[2]));
27
+ return;
28
+ }
29
+ await fs_1.default.promises.rm(path_1.default.join(constants_1.REACT_EMAIL_ROOT, filename));
22
30
  return;
23
31
  }
24
- await fs_1.default.promises.rm(path_1.default.join(constants_1.REACT_EMAIL_ROOT, filename));
25
- }
26
- else {
27
- const file = filename.split(path_1.default.sep);
28
32
  if (file[1] === 'static' && file[2]) {
29
- await (0, cpy_1.default)(`${constants_1.CLIENT_EMAILS_PATH}/static/${file[2]}`, `${constants_1.REACT_EMAIL_ROOT}/public/static`);
33
+ const srcPath = path_1.default.join(watchDir, 'static', file[2]);
34
+ await (0, cpy_1.default)(srcPath, `${constants_1.REACT_EMAIL_ROOT}/public/static`);
30
35
  return;
31
36
  }
32
- await (0, cpy_1.default)(path_1.default.join(constants_1.CURRENT_PATH, filename), path_1.default.join(constants_1.REACT_EMAIL_ROOT, file.slice(0, -1).join(path_1.default.sep)));
33
- }
34
- });
37
+ await (0, cpy_1.default)(path_1.default.join(watchDir, ...file.slice(1)), path_1.default.join(constants_1.PACKAGE_EMAILS_PATH, ...file.slice(1, -1)));
38
+ });
39
+ };
35
40
  exports.watcher = watcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-preview",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "The React Email preview application",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -12,7 +12,7 @@
12
12
  "format": "prettier --write \"**/*.{ts,tsx,md}\""
13
13
  },
14
14
  "engines": {
15
- "node": ">=18.0.0"
15
+ "node": ">=16.0.0"
16
16
  },
17
17
  "dependencies": {
18
18
  "@next/font": "13.0.4",
@@ -68,7 +68,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
68
68
  return (
69
69
  <pre
70
70
  className={
71
- 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'
71
+ 'border-slate-6 relative w-full items-center whitespace-pre rounded-md border text-sm backdrop-blur-md'
72
72
  }
73
73
  style={{
74
74
  lineHeight: '130%',
@@ -68,7 +68,7 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
68
68
  'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
69
69
  }}
70
70
  />
71
- <div className="p-4">
71
+ <div className="p-4 h-[650px] overflow-auto">
72
72
  {tokens.map((line, i) => {
73
73
  return (
74
74
  <div
@@ -7,12 +7,12 @@ export const components = [
7
7
  {
8
8
  title: 'code-container.tsx',
9
9
  content:
10
- "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [hovered, setHovered] = React.useState('');\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a\n href={url}\n download={file.name}\n className=\"text-slate-11 transition ease-in-out duration-200 hover:text-slate-12\"\n >\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"flex\">\n <AnimateSharedLayout>\n {markups.map(({ language }) => {\n const isHovered = hovered === language;\n return (\n <motion.button\n className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 ${\n activeTab !== language ? 'text-slate-11' : 'text-slate-12'\n }`}\n onClick={() => setActiveTab(language)}\n onHoverStart={() => setHovered(language)}\n onHoverEnd={() => setHovered('')}\n key={language}\n >\n {isHovered && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n {languageMap[language]}\n </motion.button>\n );\n })}\n </AnimateSharedLayout>\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
10
+ "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [hovered, setHovered] = React.useState('');\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a\n href={url}\n download={file.name}\n className=\"text-slate-11 transition ease-in-out duration-200 hover:text-slate-12\"\n >\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"flex\">\n <AnimateSharedLayout>\n {markups.map(({ language }) => {\n const isHovered = hovered === language;\n return (\n <motion.button\n className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 ${\n activeTab !== language ? 'text-slate-11' : 'text-slate-12'\n }`}\n onClick={() => setActiveTab(language)}\n onHoverStart={() => setHovered(language)}\n onHoverEnd={() => setHovered('')}\n key={language}\n >\n {isHovered && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n {languageMap[language]}\n </motion.button>\n );\n })}\n </AnimateSharedLayout>\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
11
11
  },
12
12
  {
13
13
  title: 'code.tsx',
14
14
  content:
15
- "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
15
+ "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4 h-[650px] overflow-auto\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
16
16
  },
17
17
  {
18
18
  title: 'heading.tsx',
@@ -12,7 +12,7 @@ export const root = [
12
12
  {
13
13
  title: 'package.json',
14
14
  content:
15
- '{\n "name": "react-email-preview",\n "version": "0.0.10",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.6",\n "classnames": "2.3.2",\n "framer-motion": "8.4.6",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
15
+ '{\n "name": "react-email-preview",\n "version": "0.0.11",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=16.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.6",\n "classnames": "2.3.2",\n "framer-motion": "8.4.6",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
16
16
  },
17
17
  {
18
18
  title: 'postcss.config.js',
@@ -2,8 +2,8 @@ import {
2
2
  checkDirectoryExist,
3
3
  checkEmptyDirectory,
4
4
  checkPackageIsUpToDate,
5
- CLIENT_EMAILS_PATH,
6
5
  createDirectory,
6
+ createWatcherInstance,
7
7
  CURRENT_PATH,
8
8
  PACKAGE_EMAILS_PATH,
9
9
  REACT_EMAIL_ROOT,
@@ -11,7 +11,6 @@ import {
11
11
  getPreviewPkg,
12
12
  watcher,
13
13
  PUBLIC_PATH,
14
- watcherInstance,
15
14
  } from '../utils';
16
15
  import path from 'path';
17
16
  import fs from 'fs';
@@ -27,7 +26,13 @@ import readPackage from 'read-pkg';
27
26
  import shell from 'shelljs';
28
27
  import { styles } from '../_preview/styles';
29
28
 
30
- export const dev = async () => {
29
+ interface Args {
30
+ dir: string;
31
+ }
32
+
33
+ export const dev = async ({ dir }: Args) => {
34
+ const emailDir = convertToAbsolutePath(dir);
35
+ const watcherInstance = createWatcherInstance(emailDir);
31
36
  try {
32
37
  const hasReactEmailDirectory = checkDirectoryExist(REACT_EMAIL_ROOT);
33
38
  let packageManager: PackageManager;
@@ -41,10 +46,10 @@ export const dev = async () => {
41
46
  const isUpToDate = await checkPackageIsUpToDate();
42
47
 
43
48
  if (isUpToDate) {
44
- await Promise.all([generateEmailsPreview(), syncPkg()]);
49
+ await Promise.all([generateEmailsPreview(emailDir), syncPkg()]);
45
50
  await installDependencies(packageManager);
46
51
  shell.exec(`${packageManager} run dev`, { async: true });
47
- watcher();
52
+ watcher(watcherInstance, emailDir);
48
53
  return;
49
54
  }
50
55
 
@@ -54,16 +59,19 @@ export const dev = async () => {
54
59
  await createBasicStructure();
55
60
  await createAppDirectories();
56
61
  await createAppFiles();
57
- await Promise.all([generateEmailsPreview(), syncPkg()]);
62
+ await Promise.all([generateEmailsPreview(emailDir), syncPkg()]);
58
63
  await installDependencies(packageManager);
59
64
  shell.exec(`${packageManager} run dev`, { async: true });
60
- watcher();
65
+ watcher(watcherInstance, emailDir);
61
66
  } catch (error) {
62
67
  await watcherInstance.close();
63
68
  shell.exit(1);
64
69
  }
65
70
  };
66
71
 
72
+ const convertToAbsolutePath = (dir: string): string =>
73
+ path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
74
+
67
75
  const createBasicStructure = async () => {
68
76
  try {
69
77
  // Create `.react-email` directory
@@ -129,13 +137,13 @@ const createAppFiles = async () => {
129
137
  }
130
138
  };
131
139
 
132
- const generateEmailsPreview = async () => {
140
+ const generateEmailsPreview = async (emailDir: string) => {
133
141
  try {
134
142
  const spinner = ora('Generating emails preview').start();
135
143
 
136
- await createEmailPreviews();
137
- await createStatisFiles();
138
- await createComponents();
144
+ await createEmailPreviews(emailDir);
145
+ await createStaticFiles(emailDir);
146
+ await createComponents(emailDir);
139
147
 
140
148
  spinner.stopAndPersist({
141
149
  symbol: logSymbols.success,
@@ -146,11 +154,11 @@ const generateEmailsPreview = async () => {
146
154
  }
147
155
  };
148
156
 
149
- const createEmailPreviews = async () => {
150
- const hasEmailsDirectory = checkDirectoryExist(CLIENT_EMAILS_PATH);
157
+ const createEmailPreviews = async (emailDir: string) => {
158
+ const hasEmailsDirectory = checkDirectoryExist(emailDir);
151
159
 
152
160
  const isEmailsDirectoryEmpty = hasEmailsDirectory
153
- ? await checkEmptyDirectory(CLIENT_EMAILS_PATH)
161
+ ? await checkEmptyDirectory(emailDir)
154
162
  : true;
155
163
 
156
164
  if (isEmailsDirectoryEmpty) {
@@ -162,16 +170,15 @@ const createEmailPreviews = async () => {
162
170
  await fs.promises.rm(PACKAGE_EMAILS_PATH, { recursive: true });
163
171
  }
164
172
 
165
- await copy(`${CLIENT_EMAILS_PATH}/*{.tsx,.jsx}`, PACKAGE_EMAILS_PATH);
173
+ await copy(path.join(emailDir, '*{.tsx,.jsx}'), PACKAGE_EMAILS_PATH);
166
174
  };
167
175
 
168
- const createStatisFiles = async () => {
176
+ const createStaticFiles = async (emailDir: string) => {
169
177
  const hasPackageStaticDirectory = checkDirectoryExist(
170
178
  `${REACT_EMAIL_ROOT}/public/static`,
171
179
  );
172
- const hasStaticDirectory = checkDirectoryExist(
173
- `${CLIENT_EMAILS_PATH}/static`,
174
- );
180
+ const staticDir = path.join(emailDir, 'static');
181
+ const hasStaticDirectory = checkDirectoryExist(staticDir);
175
182
 
176
183
  if (hasPackageStaticDirectory) {
177
184
  await fs.promises.rm(`${REACT_EMAIL_ROOT}/public/static`, {
@@ -180,21 +187,16 @@ const createStatisFiles = async () => {
180
187
  }
181
188
 
182
189
  if (hasStaticDirectory) {
183
- await copy(
184
- `${CLIENT_EMAILS_PATH}/static`,
185
- `${REACT_EMAIL_ROOT}/public/static`,
186
- );
190
+ await copy(staticDir, `${REACT_EMAIL_ROOT}/public/static`);
187
191
  }
188
192
  };
189
193
 
190
- const createComponents = async () => {
194
+ const createComponents = async (emailDir: string) => {
191
195
  const hasPackageComponentsDirectory = checkDirectoryExist(
192
196
  `${PACKAGE_EMAILS_PATH}/components`,
193
197
  );
194
-
195
- const hasComponentsDirectory = checkDirectoryExist(
196
- `${CLIENT_EMAILS_PATH}/components`,
197
- );
198
+ const componentDir = path.join(emailDir, 'components');
199
+ const hasComponentsDirectory = checkDirectoryExist(componentDir);
198
200
 
199
201
  if (hasPackageComponentsDirectory) {
200
202
  await fs.promises.rm(`${PACKAGE_EMAILS_PATH}/components`, {
@@ -203,10 +205,7 @@ const createComponents = async () => {
203
205
  }
204
206
 
205
207
  if (hasComponentsDirectory) {
206
- await copy(
207
- `${CLIENT_EMAILS_PATH}/components`,
208
- `${PACKAGE_EMAILS_PATH}/components`,
209
- );
208
+ await copy(componentDir, `${PACKAGE_EMAILS_PATH}/components`);
210
209
  }
211
210
  };
212
211