@better-svelte-email/preview 2.0.0-beta.0 → 2.0.0-beta.2

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/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ import { Resend } from 'resend';
2
+ import prettier from 'prettier/standalone';
3
+ import parserHtml from 'prettier/parser-html';
4
+ import { Renderer } from '@better-svelte-email/server';
5
+ import { getEmailComponent, getEmailSource } from './preview-fs';
6
+ export { default as EmailPreview } from './EmailPreview.svelte';
7
+ /**
8
+ * Import all Svelte email components file paths.
9
+ * Create a list containing all Svelte email component file names.
10
+ * Return this list to the client.
11
+ */
12
+ export { emailList, getEmailComponent, getFiles } from './preview-fs';
13
+ /**
14
+ * SvelteKit form action to render an email component.
15
+ * Use this with the Preview component to render email templates on demand.
16
+ *
17
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // +page.server.ts
22
+ * import { createEmail } from 'better-svelte-email/preview';
23
+ * import { Renderer } from 'better-svelte-email/render';
24
+ *
25
+ * const renderer = new Renderer({
26
+ * tailwindConfig: {
27
+ * theme: {
28
+ * extend: {
29
+ * colors: {
30
+ * brand: '#FF3E00'
31
+ * }
32
+ * }
33
+ * }
34
+ * }
35
+ * });
36
+ *
37
+ * export const actions = createEmail({ renderer });
38
+ * ```
39
+ */
40
+ export const createEmail = (options = {}) => {
41
+ const { renderer = new Renderer() } = options;
42
+ return {
43
+ 'create-email': async (event) => {
44
+ try {
45
+ const data = await event.request.formData();
46
+ const file = data.get('file');
47
+ const emailPath = data.get('path');
48
+ if (!file || !emailPath) {
49
+ return {
50
+ status: 400,
51
+ body: { error: 'Missing file or path parameter' }
52
+ };
53
+ }
54
+ const emailComponent = await getEmailComponent(emailPath, file);
55
+ const source = await getEmailSource(emailPath, file);
56
+ // Render the component to HTML
57
+ const html = await renderer.render(emailComponent);
58
+ // Remove all HTML comments from the body before formatting
59
+ const formattedHtml = await prettier.format(html, {
60
+ parser: 'html',
61
+ plugins: [parserHtml]
62
+ });
63
+ return {
64
+ body: formattedHtml,
65
+ source
66
+ };
67
+ }
68
+ catch (error) {
69
+ console.error('Error rendering email:', error);
70
+ return {
71
+ status: 500,
72
+ error: {
73
+ message: error instanceof Error ? error.message : 'Failed to render email'
74
+ }
75
+ };
76
+ }
77
+ }
78
+ };
79
+ };
80
+ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
81
+ // stringify api key to comment out temp
82
+ const resend = new Resend(resendApiKey);
83
+ const email = { from, to, subject, html };
84
+ const resendReq = await resend.emails.send(email);
85
+ if (resendReq.error) {
86
+ return { success: false, error: resendReq.error };
87
+ }
88
+ else {
89
+ return { success: true, error: null };
90
+ }
91
+ };
92
+ /**
93
+ * Sends the email using the submitted form data.
94
+ *
95
+ * @param options.resendApiKey - Your Resend API key (keep this server-side only)
96
+ * @param options.customSendEmailFunction - Optional custom function to send emails
97
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
98
+ * @param options.from - Optional sender email address (defaults to 'better-svelte-email <onboarding@resend.dev>')
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * // In +page.server.ts
103
+ * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
104
+ * import { Renderer } from 'better-svelte-email/render';
105
+ *
106
+ * const renderer = new Renderer({
107
+ * tailwindConfig: {
108
+ * theme: {
109
+ * extend: {
110
+ * colors: {
111
+ * brand: '#FF3E00'
112
+ * }
113
+ * }
114
+ * }
115
+ * }
116
+ * });
117
+ *
118
+ * export const actions = {
119
+ * ...createEmail({ renderer }),
120
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
121
+ * };
122
+ * ```
123
+ */
124
+ export const sendEmail = ({ customSendEmailFunction, resendApiKey, renderer = new Renderer(), from = 'better-svelte-email <onboarding@resend.dev>' } = {}) => {
125
+ return {
126
+ 'send-email': async (event) => {
127
+ const data = await event.request.formData();
128
+ const emailPath = data.get('path');
129
+ const file = data.get('file');
130
+ if (!file || !emailPath) {
131
+ return {
132
+ success: false,
133
+ error: { message: 'Missing file or path parameter' }
134
+ };
135
+ }
136
+ const emailComponent = await getEmailComponent(emailPath, file);
137
+ const email = {
138
+ from,
139
+ to: `${data.get('to')}`,
140
+ subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
141
+ html: await renderer.render(emailComponent)
142
+ };
143
+ let sent = { success: false, error: null };
144
+ if (!customSendEmailFunction && resendApiKey) {
145
+ sent = await defaultSendEmailFunction(email, resendApiKey);
146
+ }
147
+ else if (customSendEmailFunction) {
148
+ sent = await customSendEmailFunction(email);
149
+ }
150
+ else if (!customSendEmailFunction && !resendApiKey) {
151
+ const error = {
152
+ message: 'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
153
+ };
154
+ return { success: false, error };
155
+ }
156
+ if (sent && sent.error) {
157
+ console.log('Error:', sent.error);
158
+ return { success: false, error: sent.error };
159
+ }
160
+ else {
161
+ console.log('Email was sent successfully.');
162
+ return { success: true, error: null };
163
+ }
164
+ }
165
+ };
166
+ };
167
+ // EmailPreview is exported through the dedicated package subpath:
168
+ // import EmailPreview from '@better-svelte-email/preview/EmailPreview.svelte'
@@ -0,0 +1,34 @@
1
+ export type PreviewData = {
2
+ files: string[] | null;
3
+ path: string | null;
4
+ };
5
+ type EmailListProps = {
6
+ path?: string;
7
+ root?: string;
8
+ };
9
+ /**
10
+ * Get a list of all email component files in the specified directory.
11
+ *
12
+ * @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
13
+ * @param options.root - Absolute path to project root (auto-detected if not provided)
14
+ * @returns PreviewData object with list of email files and the path
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // In a +page.server.ts file
19
+ * import { emailList } from 'better-svelte-email/preview';
20
+ *
21
+ * export function load() {
22
+ * const emails = emailList({
23
+ * root: process.cwd(),
24
+ * path: '/src/lib/emails'
25
+ * });
26
+ * return { emails };
27
+ * }
28
+ * ```
29
+ */
30
+ export declare const emailList: ({ path: emailPath, root }?: EmailListProps) => PreviewData;
31
+ export declare const getEmailComponent: (emailPath: string, file: string) => Promise<any>;
32
+ export declare const getEmailSource: (emailPath: string, file: string) => Promise<string | null>;
33
+ export declare function getFiles(dir: string, files?: string[]): string[];
34
+ export {};
@@ -0,0 +1,130 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Get a list of all email component files in the specified directory.
5
+ *
6
+ * @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
7
+ * @param options.root - Absolute path to project root (auto-detected if not provided)
8
+ * @returns PreviewData object with list of email files and the path
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // In a +page.server.ts file
13
+ * import { emailList } from 'better-svelte-email/preview';
14
+ *
15
+ * export function load() {
16
+ * const emails = emailList({
17
+ * root: process.cwd(),
18
+ * path: '/src/lib/emails'
19
+ * });
20
+ * return { emails };
21
+ * }
22
+ * ```
23
+ */
24
+ export const emailList = ({ path: emailPath = '/src/lib/emails', root } = {}) => {
25
+ // If root is not provided, try to use process.cwd()
26
+ if (!root) {
27
+ try {
28
+ root = process.cwd();
29
+ }
30
+ catch (err) {
31
+ throw new Error('Could not determine the root path of your project. Please pass in the root param manually using process.cwd() or an absolute path.', { cause: err });
32
+ }
33
+ }
34
+ const fullPath = path.join(root, emailPath);
35
+ // Check if directory exists
36
+ if (!fs.existsSync(fullPath)) {
37
+ console.warn(`Email directory not found: ${fullPath}`);
38
+ return { files: null, path: emailPath };
39
+ }
40
+ // Use the absolute folder path as the root when creating the component list so
41
+ // we can compute correct relative paths on all platforms.
42
+ const files = createEmailComponentList(fullPath, getFiles(fullPath));
43
+ if (!files.length) {
44
+ return { files: null, path: emailPath };
45
+ }
46
+ return { files, path: emailPath };
47
+ };
48
+ export const getEmailComponent = async (emailPath, file) => {
49
+ const fileName = `${file}.svelte`;
50
+ try {
51
+ // Import the email component dynamically
52
+ const normalizedEmailPath = emailPath.replace(/\\/g, '/').replace(/\/+$/, '');
53
+ const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
54
+ const importPath = `${normalizedEmailPath}/${normalizedFile}.svelte`;
55
+ return (await import(/* @vite-ignore */ importPath)).default;
56
+ }
57
+ catch (err) {
58
+ throw new Error(`Failed to import email component '${fileName}'. Make sure the file exists and includes the <Head /> component.`, { cause: err });
59
+ }
60
+ };
61
+ export const getEmailSource = async (emailPath, file) => {
62
+ const normalizedEmailPath = emailPath.replace(/\\/g, '/').replace(/\/+$/, '');
63
+ const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
64
+ const candidates = new Set();
65
+ const relativeEmailPath = normalizedEmailPath.replace(/^\/+/, '');
66
+ if (normalizedEmailPath) {
67
+ candidates.add(path.resolve(process.cwd(), relativeEmailPath, `${normalizedFile}.svelte`));
68
+ candidates.add(path.resolve(process.cwd(), normalizedEmailPath, `${normalizedFile}.svelte`));
69
+ candidates.add(path.resolve(normalizedEmailPath, `${normalizedFile}.svelte`));
70
+ }
71
+ candidates.add(path.resolve(process.cwd(), `${normalizedFile}.svelte`));
72
+ for (const candidate of candidates) {
73
+ try {
74
+ return await fs.promises.readFile(candidate, 'utf8');
75
+ }
76
+ catch {
77
+ // continue to next candidate
78
+ }
79
+ }
80
+ console.warn(`Source file not found for ${normalizedFile} in ${normalizedEmailPath}`);
81
+ return null;
82
+ };
83
+ // Recursive function to get files
84
+ export function getFiles(dir, files = []) {
85
+ // Get an array of all files and directories in the passed directory using fs.readdirSync
86
+ const fileList = fs.readdirSync(dir);
87
+ // Create the full path of the file/directory by concatenating the passed directory and file/directory name
88
+ for (const file of fileList) {
89
+ const name = path.join(dir, file);
90
+ // Check if the current file/directory is a directory using fs.statSync
91
+ if (fs.statSync(name).isDirectory()) {
92
+ // If it is a directory, recursively call the getFiles function with the directory path and the files array
93
+ getFiles(name, files);
94
+ }
95
+ else {
96
+ // If it is a file, push the full path to the files array
97
+ files.push(name);
98
+ }
99
+ }
100
+ return files;
101
+ }
102
+ /**
103
+ * Creates an array of names from the record of svelte email component file paths
104
+ */
105
+ function createEmailComponentList(root, paths) {
106
+ const emailComponentList = [];
107
+ paths.forEach((filePath) => {
108
+ if (filePath.endsWith('.svelte')) {
109
+ // Get the directory name from the full path
110
+ const fileDir = path.dirname(filePath);
111
+ // Get the base name without extension
112
+ const baseName = path.basename(filePath, '.svelte');
113
+ // Normalize paths for cross-platform comparison
114
+ const rootNormalized = path.normalize(root);
115
+ const fileDirNormalized = path.normalize(fileDir);
116
+ // Find where root appears in the full directory path
117
+ const rootIndex = fileDirNormalized.indexOf(rootNormalized);
118
+ if (rootIndex !== -1) {
119
+ // Get everything after the root path
120
+ const afterRoot = fileDirNormalized.substring(rootIndex + rootNormalized.length);
121
+ // Combine with the base name using path.join for proper separators
122
+ const relativePath = afterRoot ? path.join(afterRoot, baseName) : baseName;
123
+ // Remove leading path separators
124
+ const cleanPath = relativePath.replace(/^[/\\]+/, '');
125
+ emailComponentList.push(cleanPath);
126
+ }
127
+ }
128
+ });
129
+ return emailComponentList;
130
+ }
package/dist/theme.css ADDED
@@ -0,0 +1,42 @@
1
+ :root {
2
+ --background: oklch(98.5% 0.001 106.423);
3
+ --foreground: oklch(21.6% 0.006 56.043);
4
+ --card: oklch(1 0 0);
5
+ --card-foreground: oklch(0.147 0.004 49.25);
6
+ --popover: oklch(1 0 0);
7
+ --popover-foreground: oklch(0.147 0.004 49.25);
8
+ --primary: oklch(0.216 0.006 56.043);
9
+ --primary-foreground: oklch(0.985 0.001 106.423);
10
+ --secondary: oklch(0.958 0.003 48.717);
11
+ --secondary-foreground: oklch(0.454 0.01 67.558);
12
+ --muted: oklch(0.9483 0.0061 67.75);
13
+ --muted-foreground: oklch(0.4761 0.021783 55.8952);
14
+ --accent: oklch(0.97 0.001 106.424);
15
+ --accent-foreground: oklch(0.216 0.006 56.043);
16
+ --destructive: oklch(0.577 0.245 27.325);
17
+ --border: oklch(0.923 0.003 48.717);
18
+ --input: oklch(0.923 0.003 48.717);
19
+ --ring: oklch(0.709 0.01 56.259);
20
+ --svelte: #f73b01;
21
+ }
22
+
23
+ .dark {
24
+ --background: oklch(14.7% 0.004 49.25);
25
+ --foreground: oklch(97% 0.001 106.424);
26
+ --card: oklch(0.216 0.006 56.043);
27
+ --card-foreground: oklch(0.985 0.001 106.423);
28
+ --popover: oklch(0.216 0.006 56.043);
29
+ --popover-foreground: oklch(0.985 0.001 106.423);
30
+ --primary: oklch(0.923 0.003 48.717);
31
+ --primary-foreground: oklch(0.216 0.006 56.043);
32
+ --secondary: oklch(0.216 0.006 56.043);
33
+ --secondary-foreground: oklch(0.709 0.01 56.259);
34
+ --muted: oklch(0.268 0.007 34.298);
35
+ --muted-foreground: oklch(0.7348 0.0326 67.28);
36
+ --accent: oklch(0.268 0.007 34.298);
37
+ --accent-foreground: oklch(0.985 0.001 106.423);
38
+ --destructive: oklch(0.704 0.191 22.216);
39
+ --border: oklch(1 0 0 / 10%);
40
+ --input: oklch(1 0 0 / 15%);
41
+ --ring: oklch(0.553 0.013 58.071);
42
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@better-svelte-email/preview",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.2",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "dependencies": {
7
- "@better-svelte-email/server": "workspace:*",
7
+ "@better-svelte-email/server": "2.0.0-beta.2",
8
8
  "prettier": "^3.8.1",
9
9
  "resend": "^6.8.0"
10
10
  },