promote-email-templates 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/index.d.mts +8 -1
  2. package/dist/index.d.ts +8 -1
  3. package/dist/index.js +27 -3
  4. package/dist/index.mjs +20 -0
  5. package/package.json +17 -14
  6. package/.react-email/.eslintrc.js +0 -52
  7. package/.react-email/.prettierignore +0 -3
  8. package/.react-email/.prettierrc.js +0 -8
  9. package/.react-email/license.md +0 -7
  10. package/.react-email/next.config.js +0 -36
  11. package/.react-email/package.json +0 -1
  12. package/.react-email/postcss.config.js +0 -8
  13. package/.react-email/readme.md +0 -44
  14. package/.react-email/src/actions/get-email-path-from-slug.ts +0 -26
  15. package/.react-email/src/actions/get-emails-directory-metadata.spec.ts +0 -73
  16. package/.react-email/src/actions/get-emails-directory-metadata.ts +0 -91
  17. package/.react-email/src/actions/render-email-by-path.tsx +0 -59
  18. package/.react-email/src/app/favicon.ico +0 -0
  19. package/.react-email/src/app/globals.css +0 -35
  20. package/.react-email/src/app/inter.ts +0 -7
  21. package/.react-email/src/app/layout.tsx +0 -36
  22. package/.react-email/src/app/logo.png +0 -0
  23. package/.react-email/src/app/page.tsx +0 -47
  24. package/.react-email/src/app/preview/[...slug]/page.tsx +0 -65
  25. package/.react-email/src/app/preview/[...slug]/preview.tsx +0 -141
  26. package/.react-email/src/app/preview/[...slug]/rendering-error.tsx +0 -40
  27. package/.react-email/src/components/button.tsx +0 -90
  28. package/.react-email/src/components/code-container.tsx +0 -145
  29. package/.react-email/src/components/code.tsx +0 -112
  30. package/.react-email/src/components/heading.tsx +0 -113
  31. package/.react-email/src/components/icons/icon-arrow-down.tsx +0 -16
  32. package/.react-email/src/components/icons/icon-base.tsx +0 -24
  33. package/.react-email/src/components/icons/icon-button.tsx +0 -23
  34. package/.react-email/src/components/icons/icon-check.tsx +0 -19
  35. package/.react-email/src/components/icons/icon-clipboard.tsx +0 -40
  36. package/.react-email/src/components/icons/icon-download.tsx +0 -19
  37. package/.react-email/src/components/icons/icon-file.tsx +0 -19
  38. package/.react-email/src/components/icons/icon-folder-open.tsx +0 -19
  39. package/.react-email/src/components/icons/icon-folder.tsx +0 -18
  40. package/.react-email/src/components/icons/icon-hide-sidebar.tsx +0 -23
  41. package/.react-email/src/components/icons/icon-monitor.tsx +0 -19
  42. package/.react-email/src/components/icons/icon-phone.tsx +0 -26
  43. package/.react-email/src/components/icons/icon-source.tsx +0 -19
  44. package/.react-email/src/components/index.ts +0 -7
  45. package/.react-email/src/components/logo.tsx +0 -64
  46. package/.react-email/src/components/send.tsx +0 -135
  47. package/.react-email/src/components/shell.tsx +0 -115
  48. package/.react-email/src/components/sidebar/index.ts +0 -1
  49. package/.react-email/src/components/sidebar/sidebar-directory-children.tsx +0 -134
  50. package/.react-email/src/components/sidebar/sidebar-directory.tsx +0 -106
  51. package/.react-email/src/components/sidebar/sidebar.tsx +0 -45
  52. package/.react-email/src/components/text.tsx +0 -99
  53. package/.react-email/src/components/tooltip-content.tsx +0 -32
  54. package/.react-email/src/components/tooltip.tsx +0 -19
  55. package/.react-email/src/components/topbar.tsx +0 -161
  56. package/.react-email/src/contexts/emails.tsx +0 -127
  57. package/.react-email/src/hooks/use-hot-reload.ts +0 -35
  58. package/.react-email/src/hooks/use-rendering-metadata.ts +0 -36
  59. package/.react-email/src/utils/cn.ts +0 -6
  60. package/.react-email/src/utils/constants.ts +0 -6
  61. package/.react-email/src/utils/copy-text-to-clipboard.ts +0 -7
  62. package/.react-email/src/utils/emails-directory-absolute-path.ts +0 -34
  63. package/.react-email/src/utils/get-email-component.ts +0 -108
  64. package/.react-email/src/utils/improve-error-with-sourcemap.ts +0 -55
  65. package/.react-email/src/utils/index.ts +0 -5
  66. package/.react-email/src/utils/language-map.ts +0 -7
  67. package/.react-email/src/utils/static-node-modules-for-vm.ts +0 -92
  68. package/.react-email/src/utils/types/as.ts +0 -26
  69. package/.react-email/src/utils/types/email-template.ts +0 -8
  70. package/.react-email/src/utils/types/error-object.ts +0 -11
  71. package/.react-email/src/utils/types/hot-reload-change.ts +0 -6
  72. package/.react-email/src/utils/types/hot-reload-event.ts +0 -6
  73. package/.react-email/src/utils/unreachable.ts +0 -8
  74. package/.react-email/tailwind.config.ts +0 -94
@@ -1,99 +0,0 @@
1
- import * as SlotPrimitive from '@radix-ui/react-slot';
2
- import * as React from 'react';
3
- import { type As, unreachable, cn } from '../utils';
4
-
5
- export type TextSize = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
6
- export type TextColor = 'gray' | 'white';
7
- export type TextTransform = 'uppercase' | 'lowercase' | 'capitalize';
8
- export type TextWeight = 'normal' | 'medium';
9
-
10
- interface TextOwnProps {
11
- size?: TextSize;
12
- color?: TextColor;
13
- transform?: TextTransform;
14
- weight?: TextWeight;
15
- }
16
-
17
- type TextProps = As<'span', 'div', 'p'> & TextOwnProps;
18
-
19
- export const Text = React.forwardRef<HTMLSpanElement, Readonly<TextProps>>(
20
- (
21
- {
22
- as: Tag = 'span',
23
- size = '2',
24
- color = 'gray',
25
- transform,
26
- weight = 'normal',
27
- className,
28
- children,
29
- ...props
30
- },
31
- forwardedRef,
32
- ) => (
33
- <SlotPrimitive.Slot
34
- className={cn(
35
- className,
36
- transform,
37
- getSizesClassNames(size),
38
- getColorClassNames(color),
39
- getWeightClassNames(weight),
40
- )}
41
- ref={forwardedRef}
42
- {...props}
43
- >
44
- <Tag>{children}</Tag>
45
- </SlotPrimitive.Slot>
46
- ),
47
- );
48
-
49
- const getSizesClassNames = (size: TextSize | undefined) => {
50
- switch (size) {
51
- case '1':
52
- return 'text-xs';
53
- case undefined:
54
- case '2':
55
- return 'text-sm';
56
- case '3':
57
- return 'text-base';
58
- case '4':
59
- return 'text-lg';
60
- case '5':
61
- return ['text-17px', 'md:text-xl tracking-[-0.16px]'];
62
- case '6':
63
- return 'text-2xl tracking-[-0.288px]';
64
- case '7':
65
- return 'text-[28px] leading-[34px] tracking-[-0.416px]';
66
- case '8':
67
- return 'text-[35px] leading-[42px] tracking-[-0.64px]';
68
- case '9':
69
- return 'text-6xl leading-[73px] tracking-[-0.896px]';
70
- default:
71
- return unreachable(size);
72
- }
73
- };
74
-
75
- const getColorClassNames = (color: TextColor | undefined) => {
76
- switch (color) {
77
- case 'white':
78
- return 'text-slate-12';
79
- case undefined:
80
- case 'gray':
81
- return 'text-slate-11';
82
- default:
83
- return unreachable(color);
84
- }
85
- };
86
-
87
- const getWeightClassNames = (weight: TextWeight | undefined) => {
88
- switch (weight) {
89
- case undefined:
90
- case 'normal':
91
- return 'font-normal';
92
- case 'medium':
93
- return 'font-medium';
94
- default:
95
- return unreachable(weight);
96
- }
97
- };
98
-
99
- Text.displayName = 'Text';
@@ -1,32 +0,0 @@
1
- import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
- import * as React from 'react';
3
- import { cn } from '../utils';
4
- import { inter } from '../app/inter';
5
-
6
- type ContentElement = React.ElementRef<typeof TooltipPrimitive.Content>;
7
- type ContentProps = React.ComponentPropsWithoutRef<
8
- typeof TooltipPrimitive.Content
9
- >;
10
-
11
- export type TooltipProps = ContentProps;
12
-
13
- export const TooltipContent = React.forwardRef<
14
- ContentElement,
15
- Readonly<TooltipProps>
16
- >(({ sideOffset = 6, children, ...props }, forwardedRef) => (
17
- <TooltipPrimitive.Portal>
18
- <TooltipPrimitive.Content
19
- {...props}
20
- className={cn(
21
- 'bg-black border border-slate-6 z-20 px-3 py-2 rounded-md text-xs',
22
- `${inter.variable} font-sans`,
23
- )}
24
- ref={forwardedRef}
25
- sideOffset={sideOffset}
26
- >
27
- {children}
28
- </TooltipPrimitive.Content>
29
- </TooltipPrimitive.Portal>
30
- ));
31
-
32
- TooltipContent.displayName = 'TooltipContent';
@@ -1,19 +0,0 @@
1
- import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
- import * as React from 'react';
3
- import { TooltipContent } from './tooltip-content';
4
-
5
- type RootProps = React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>;
6
-
7
- export type TooltipProps = RootProps;
8
-
9
- export const TooltipRoot: React.FC<Readonly<TooltipProps>> = ({
10
- children,
11
- ...props
12
- }) => <TooltipPrimitive.Root {...props}>{children}</TooltipPrimitive.Root>;
13
-
14
- export const Tooltip = Object.assign(TooltipRoot, {
15
- Arrow: TooltipPrimitive.TooltipArrow,
16
- Provider: TooltipPrimitive.TooltipProvider,
17
- Content: TooltipContent,
18
- Trigger: TooltipPrimitive.TooltipTrigger,
19
- });
@@ -1,161 +0,0 @@
1
- 'use client';
2
- import * as ToggleGroup from '@radix-ui/react-toggle-group';
3
- import { motion } from 'framer-motion';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
- import { tabTransition } from '../utils/constants';
7
- import { Heading } from './heading';
8
- import { IconHideSidebar } from './icons/icon-hide-sidebar';
9
- import { IconMonitor } from './icons/icon-monitor';
10
- import { IconPhone } from './icons/icon-phone';
11
- import { IconSource } from './icons/icon-source';
12
- import { Send } from './send';
13
- import { Tooltip } from './tooltip';
14
-
15
- interface TopbarProps {
16
- currentEmailOpenSlug: string;
17
- activeView?: string;
18
- markup?: string;
19
- onToggleSidebar?: () => void;
20
- setActiveView?: (view: string) => void;
21
- }
22
-
23
- export const Topbar: React.FC<Readonly<TopbarProps>> = ({
24
- currentEmailOpenSlug,
25
- markup,
26
- activeView,
27
- setActiveView,
28
- onToggleSidebar,
29
- }) => {
30
- return (
31
- <Tooltip.Provider>
32
- <header className="flex relative items-center px-4 justify-between h-[70px] border-b border-slate-6">
33
- <Tooltip>
34
- <Tooltip.Trigger asChild>
35
- <button
36
- className="hidden lg:flex rounded-lg px-2 py-2 transition ease-in-out duration-200 relative hover:bg-slate-5 text-slate-11 hover:text-slate-12"
37
- onClick={() => {
38
- if (onToggleSidebar) {
39
- onToggleSidebar();
40
- }
41
- }}
42
- type="button"
43
- >
44
- <IconHideSidebar height={20} width={20} />
45
- </button>
46
- </Tooltip.Trigger>
47
- <Tooltip.Content>Show/hide sidebar</Tooltip.Content>
48
- </Tooltip>
49
-
50
- <div className="items-center overflow-hidden hidden lg:flex text-center absolute left-1/2 transform -translate-x-1/2 top-1/2 -translate-y-1/2">
51
- <Heading as="h2" className="truncate" size="2" weight="medium">
52
- {currentEmailOpenSlug}
53
- </Heading>
54
- </div>
55
-
56
- <div className="flex gap-3 justify-between lg:justify-start w-full lg:w-fit">
57
- <ToggleGroup.Root
58
- aria-label="View mode"
59
- className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
60
- onValueChange={(value) => {
61
- if (value) setActiveView?.(value);
62
- }}
63
- type="single"
64
- value={activeView}
65
- >
66
- <ToggleGroup.Item value="desktop">
67
- <Tooltip>
68
- <Tooltip.Trigger asChild>
69
- <div
70
- className={cn(
71
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
72
- {
73
- 'text-slate-11': activeView !== 'desktop',
74
- 'text-slate-12': activeView === 'desktop',
75
- },
76
- )}
77
- >
78
- {activeView === 'desktop' && (
79
- <motion.span
80
- animate={{ opacity: 1 }}
81
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
82
- exit={{ opacity: 0 }}
83
- initial={{ opacity: 0 }}
84
- layoutId="topbar-tabs"
85
- transition={tabTransition}
86
- />
87
- )}
88
- <IconMonitor />
89
- </div>
90
- </Tooltip.Trigger>
91
- <Tooltip.Content>Desktop</Tooltip.Content>
92
- </Tooltip>
93
- </ToggleGroup.Item>
94
- <ToggleGroup.Item value="mobile">
95
- <Tooltip>
96
- <Tooltip.Trigger asChild>
97
- <div
98
- className={cn(
99
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
100
- {
101
- 'text-slate-11': activeView !== 'mobile',
102
- 'text-slate-12': activeView === 'mobile',
103
- },
104
- )}
105
- >
106
- {activeView === 'mobile' && (
107
- <motion.span
108
- animate={{ opacity: 1 }}
109
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
110
- exit={{ opacity: 0 }}
111
- initial={{ opacity: 0 }}
112
- layoutId="topbar-tabs"
113
- transition={tabTransition}
114
- />
115
- )}
116
- <IconPhone />
117
- </div>
118
- </Tooltip.Trigger>
119
- <Tooltip.Content>Mobile</Tooltip.Content>
120
- </Tooltip>
121
- </ToggleGroup.Item>
122
- <ToggleGroup.Item value="source">
123
- <Tooltip>
124
- <Tooltip.Trigger asChild>
125
- <div
126
- className={cn(
127
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
128
- {
129
- 'text-slate-11': activeView !== 'source',
130
- 'text-slate-12': activeView === 'source',
131
- },
132
- )}
133
- >
134
- {activeView === 'source' && (
135
- <motion.span
136
- animate={{ opacity: 1 }}
137
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
138
- exit={{ opacity: 0 }}
139
- initial={{ opacity: 0 }}
140
- layoutId="topbar-tabs"
141
- transition={tabTransition}
142
- />
143
- )}
144
- <IconSource />
145
- </div>
146
- </Tooltip.Trigger>
147
- <Tooltip.Content>Code</Tooltip.Content>
148
- </Tooltip>
149
- </ToggleGroup.Item>
150
- </ToggleGroup.Root>
151
-
152
- {markup ? (
153
- <div className="flex justify-end">
154
- <Send markup={markup} />
155
- </div>
156
- ) : null}
157
- </div>
158
- </header>
159
- </Tooltip.Provider>
160
- );
161
- };
@@ -1,127 +0,0 @@
1
- 'use client';
2
- import { createContext, useContext, useEffect, useState } from 'react';
3
- import {
4
- getEmailsDirectoryMetadata,
5
- type EmailsDirectory,
6
- } from '../actions/get-emails-directory-metadata';
7
- import { useHotreload } from '../hooks/use-hot-reload';
8
- import {
9
- emailsDirRelativePath,
10
- emailsDirectoryAbsolutePath,
11
- normalizePath,
12
- pathSeparator,
13
- } from '../utils/emails-directory-absolute-path';
14
- import {
15
- renderEmailByPath,
16
- type EmailRenderingResult,
17
- } from '../actions/render-email-by-path';
18
- import { getEmailPathFromSlug } from '../actions/get-email-path-from-slug';
19
-
20
- const EmailsContext = createContext<
21
- | {
22
- emailsDirectoryMetadata: EmailsDirectory;
23
- /**
24
- * Uses the hot reloaded bundled build and rendering email result
25
- */
26
- useEmailRenderingResult: (
27
- emailPath: string,
28
- serverEmailRenderedResult: EmailRenderingResult,
29
- ) => EmailRenderingResult;
30
- }
31
- | undefined
32
- >(undefined);
33
-
34
- export const useEmails = () => {
35
- const providerValue = useContext(EmailsContext);
36
-
37
- if (typeof providerValue === 'undefined') {
38
- throw new Error(
39
- 'Cannot call `useEmail()` outside of an EmailsContext provider!',
40
- );
41
- }
42
-
43
- return providerValue;
44
- };
45
-
46
- export const EmailsProvider = (props: {
47
- initialEmailsDirectoryMetadata: EmailsDirectory;
48
- children: React.ReactNode;
49
- }) => {
50
- const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] =
51
- useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);
52
-
53
- const [renderingResultPerEmailPath, setRenderingResultPerEmailPath] =
54
- useState<Record<string, EmailRenderingResult>>({});
55
-
56
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
57
- // this will not change on runtime so it doesn't violate
58
- // the rules of hooks
59
- // eslint-disable-next-line react-hooks/rules-of-hooks
60
- useHotreload(async (changes) => {
61
- const metadata = await getEmailsDirectoryMetadata(
62
- emailsDirectoryAbsolutePath,
63
- );
64
- if (metadata) {
65
- setEmailsDirectoryMetadata(metadata);
66
- } else {
67
- throw new Error(
68
- 'Hot reloading: unable to find the emails directory to update the sidebar',
69
- );
70
- }
71
-
72
- for await (const change of changes) {
73
- const normalizedEmailsDirRelativePath = normalizePath(
74
- emailsDirRelativePath,
75
- );
76
- const slugForChangedEmail =
77
- // filename ex: emails/apple-receipt.tsx
78
- // so we need to remove the "emails/" because it isn't used
79
- // on the slug parameter for the preview page
80
- change.filename
81
- .replace(`${normalizedEmailsDirRelativePath}${pathSeparator}`, '')
82
- .replace(normalizedEmailsDirRelativePath, '');
83
-
84
- const pathForChangedEmail =
85
- await getEmailPathFromSlug(slugForChangedEmail);
86
-
87
- const lastResult = renderingResultPerEmailPath[pathForChangedEmail];
88
-
89
- if (typeof lastResult !== 'undefined') {
90
- const renderingResult = await renderEmailByPath(pathForChangedEmail);
91
-
92
- setRenderingResultPerEmailPath((map) => ({
93
- ...map,
94
- [pathForChangedEmail]: renderingResult,
95
- }));
96
- }
97
- }
98
- });
99
- }
100
-
101
- return (
102
- <EmailsContext.Provider
103
- value={{
104
- emailsDirectoryMetadata,
105
- useEmailRenderingResult: (emailPath, serverEmailRenderedResult) => {
106
- useEffect(() => {
107
- if (typeof renderingResultPerEmailPath[emailPath] === 'undefined') {
108
- setRenderingResultPerEmailPath((map) => ({
109
- ...map,
110
- [emailPath]: serverEmailRenderedResult,
111
- }));
112
- }
113
- }, [serverEmailRenderedResult, emailPath]);
114
-
115
- if (typeof renderingResultPerEmailPath[emailPath] !== 'undefined') {
116
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
117
- return renderingResultPerEmailPath[emailPath]!;
118
- }
119
-
120
- return serverEmailRenderedResult;
121
- },
122
- }}
123
- >
124
- {props.children}
125
- </EmailsContext.Provider>
126
- );
127
- };
@@ -1,35 +0,0 @@
1
- 'use client';
2
- import { useEffect, useRef } from 'react';
3
- import { type Socket, io } from 'socket.io-client';
4
- import type { HotReloadChange } from '../utils/types/hot-reload-change';
5
-
6
- /**
7
- * Hook that detects any "reload" event sent from the CLI's web socket
8
- * and calls the received parameter callback
9
- */
10
- export const useHotreload = (
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- onShouldReload: (changes: HotReloadChange[]) => any,
13
- ) => {
14
- const socketRef = useRef<Socket | null>(null);
15
-
16
- useEffect(() => {
17
- if (process.env.NODE_ENV === 'production') {
18
- return;
19
- }
20
-
21
- if (!socketRef.current) {
22
- socketRef.current = io();
23
- }
24
- const socket = socketRef.current;
25
-
26
- socket.on('reload', (changes: HotReloadChange[]) => {
27
- console.debug('Reloading...');
28
- void onShouldReload(changes);
29
- });
30
-
31
- return () => {
32
- socket.off();
33
- };
34
- }, [onShouldReload]);
35
- };
@@ -1,36 +0,0 @@
1
- import { useEffect } from 'react';
2
- import type {
3
- EmailRenderingResult,
4
- RenderedEmailMetadata,
5
- } from '../actions/render-email-by-path';
6
-
7
- const lastRenderingMetadataPerEmailPath = {} as Record<
8
- string,
9
- RenderedEmailMetadata
10
- >;
11
-
12
- /**
13
- * Returns the rendering metadata if the given `renderingResult`
14
- * does not error. If it does error it returns the last value it had for the hook.
15
- */
16
- export const useRenderingMetadata = (
17
- emailPath: string,
18
- renderingResult: EmailRenderingResult,
19
- initialRenderingMetadata?: EmailRenderingResult,
20
- ): RenderedEmailMetadata | undefined => {
21
- useEffect(() => {
22
- if ('markup' in renderingResult) {
23
- lastRenderingMetadataPerEmailPath[emailPath] = renderingResult;
24
- } else if (
25
- typeof initialRenderingMetadata !== 'undefined' &&
26
- 'markup' in initialRenderingMetadata &&
27
- typeof lastRenderingMetadataPerEmailPath[emailPath] === 'undefined'
28
- ) {
29
- lastRenderingMetadataPerEmailPath[emailPath] = initialRenderingMetadata;
30
- }
31
- }, [renderingResult, emailPath, initialRenderingMetadata]);
32
-
33
- return 'error' in renderingResult
34
- ? lastRenderingMetadataPerEmailPath[emailPath]
35
- : renderingResult;
36
- };
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export const cn = (...inputs: ClassValue[]) => {
5
- return twMerge(clsx(inputs));
6
- };
@@ -1,6 +0,0 @@
1
- export const tabTransition = {
2
- type: 'spring',
3
- stiffness: 2000,
4
- damping: 80,
5
- mass: 1,
6
- };
@@ -1,7 +0,0 @@
1
- export const copyTextToClipboard = async (text: string) => {
2
- try {
3
- await navigator.clipboard.writeText(text);
4
- } catch {
5
- throw new Error('Not able to copy');
6
- }
7
- };
@@ -1,34 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
- export const emailsDirRelativePath =
3
- process.env.NEXT_PUBLIC_EMAILS_DIR_RELATIVE_PATH ?? 'emails';
4
-
5
- export const userProjectLocation =
6
- process.env.NEXT_PUBLIC_USER_PROJECT_LOCATION!;
7
-
8
- // this trickery to find the path separator for the OS is for this to work both on the client
9
- // and on the server properly
10
- export const pathSeparator = process.env.NEXT_PUBLIC_OS_PATH_SEPARATOR! as
11
- | '/'
12
- | '\\';
13
-
14
- export const normalizePath = (path: string) => {
15
- let newPath = path;
16
-
17
- while (newPath.startsWith(`.${pathSeparator}`)) {
18
- newPath = newPath.slice(2);
19
- }
20
-
21
- while (newPath.startsWith(pathSeparator)) {
22
- newPath = newPath.slice(1);
23
- }
24
-
25
- while (newPath.endsWith(pathSeparator)) {
26
- newPath = newPath.slice(0, -1);
27
- }
28
-
29
- return newPath;
30
- };
31
-
32
- export const emailsDirectoryAbsolutePath = `${
33
- process.env.NEXT_PUBLIC_USER_PROJECT_LOCATION
34
- }${pathSeparator}${normalizePath(emailsDirRelativePath)}`;
@@ -1,108 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
- import path from 'node:path';
3
- import vm from 'node:vm';
4
- import { type RawSourceMap } from 'source-map-js';
5
- import { type OutputFile, build, type BuildFailure } from 'esbuild';
6
- import type { EmailTemplate as EmailComponent } from './types/email-template';
7
- import type { ErrorObject } from './types/error-object';
8
- import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
9
- import { staticNodeModulesForVM } from './static-node-modules-for-vm';
10
-
11
- export const getEmailComponent = async (
12
- emailPath: string,
13
- ): Promise<
14
- | {
15
- emailComponent: EmailComponent;
16
-
17
- sourceMapToOriginalFile: RawSourceMap;
18
- }
19
- | { error: ErrorObject }
20
- > => {
21
- let outputFiles: OutputFile[];
22
- try {
23
- const buildData = await build({
24
- bundle: true,
25
- entryPoints: [emailPath],
26
- platform: 'node',
27
- write: false,
28
- format: 'cjs',
29
- jsx: 'automatic',
30
- logLevel: 'silent',
31
- // allows for using jsx on a .js file
32
- loader: {
33
- '.js': 'jsx',
34
- },
35
- outdir: 'stdout', // just a stub for esbuild, it won't actually write to this folder
36
- sourcemap: 'external',
37
- });
38
- outputFiles = buildData.outputFiles;
39
- } catch (exception) {
40
- const buildFailure = exception as BuildFailure;
41
- return {
42
- error: {
43
- message: buildFailure.message,
44
- stack: buildFailure.stack,
45
- name: buildFailure.name,
46
- cause: buildFailure.cause,
47
- },
48
- };
49
- }
50
-
51
- const sourceMapFile = outputFiles[0]!;
52
- const bundledEmailFile = outputFiles[1]!;
53
- const builtEmailCode = bundledEmailFile.text;
54
-
55
- const fakeContext = {
56
- ...global,
57
- console,
58
- Buffer,
59
- module: { exports: { default: undefined as unknown } },
60
- __filanem: emailPath,
61
- __dirname: path.dirname(emailPath),
62
- require: (module: string) => {
63
- if (module in staticNodeModulesForVM) {
64
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
65
- return staticNodeModulesForVM[module];
66
- }
67
-
68
- // eslint-disable-next-line @typescript-eslint/no-var-requires
69
- return require(`${module}`) as unknown;
70
- // this stupid string templating was necessary to not have
71
- // webpack warnings like:
72
- //
73
- // Import trace for requested module:
74
- // ./src/utils/get-email-component.tsx
75
- // ./src/app/page.tsx
76
- // ⚠ ./src/utils/get-email-component.tsx
77
- // Critical dependency: the request of a dependency is an expression
78
- },
79
- process,
80
- };
81
- const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
82
- try {
83
- vm.runInNewContext(builtEmailCode, fakeContext, { filename: emailPath });
84
- } catch (exception) {
85
- const error = exception as Error;
86
-
87
- return {
88
- error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
89
- };
90
- }
91
-
92
- if (fakeContext.module.exports.default === undefined) {
93
- return {
94
- error: improveErrorWithSourceMap(
95
- new Error(
96
- `The email component at ${emailPath} does not contain a default export`,
97
- ),
98
- emailPath,
99
- sourceMapToEmail,
100
- ),
101
- };
102
- }
103
-
104
- return {
105
- emailComponent: fakeContext.module.exports.default as EmailComponent,
106
- sourceMapToOriginalFile: sourceMapToEmail,
107
- };
108
- };