better-svelte-email 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/Body.svelte +11 -2
  2. package/dist/components/Button.svelte +2 -4
  3. package/dist/components/Column.svelte +14 -0
  4. package/dist/components/Column.svelte.d.ts +7 -0
  5. package/dist/components/Container.svelte +5 -6
  6. package/dist/components/Container.svelte.d.ts +2 -2
  7. package/dist/components/Heading.svelte +34 -0
  8. package/dist/components/Heading.svelte.d.ts +15 -0
  9. package/dist/components/Hr.svelte +4 -4
  10. package/dist/components/Html.svelte +5 -4
  11. package/dist/components/Html.svelte.d.ts +3 -3
  12. package/dist/components/Img.svelte +27 -0
  13. package/dist/components/Img.svelte.d.ts +10 -0
  14. package/dist/components/Link.svelte +22 -0
  15. package/dist/components/Link.svelte.d.ts +9 -0
  16. package/dist/components/Preview.svelte +35 -0
  17. package/dist/components/Preview.svelte.d.ts +7 -0
  18. package/dist/components/Row.svelte +26 -0
  19. package/dist/components/Row.svelte.d.ts +7 -0
  20. package/dist/components/Section.svelte +11 -3
  21. package/dist/components/Section.svelte.d.ts +2 -2
  22. package/dist/components/Text.svelte +4 -6
  23. package/dist/components/Text.svelte.d.ts +0 -1
  24. package/dist/components/index.d.ts +6 -0
  25. package/dist/components/index.js +6 -0
  26. package/dist/emails/apple-receipt.svelte +387 -0
  27. package/dist/{components/__tests__/test-email.svelte.d.ts → emails/apple-receipt.svelte.d.ts} +6 -14
  28. package/dist/emails/demo-email.svelte +1 -1
  29. package/dist/emails/test-email.svelte +4 -2
  30. package/dist/emails/vercel-invite-user.svelte +136 -0
  31. package/dist/emails/vercel-invite-user.svelte.d.ts +14 -0
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +2 -2
  34. package/dist/preprocessor/index.js +3 -2
  35. package/dist/preprocessor/transformer.js +3 -3
  36. package/dist/preview/EmailPreview.svelte +773 -0
  37. package/dist/preview/EmailPreview.svelte.d.ts +7 -0
  38. package/dist/preview/index.d.ts +103 -0
  39. package/dist/preview/index.js +205 -0
  40. package/dist/utils/index.d.ts +27 -0
  41. package/dist/utils/index.js +47 -0
  42. package/package.json +34 -7
  43. package/dist/components/__tests__/test-email.svelte +0 -13
@@ -0,0 +1,7 @@
1
+ import type { PreviewData } from './index.js';
2
+ type $$ComponentProps = {
3
+ emailList: PreviewData;
4
+ };
5
+ declare const EmailPreview: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EmailPreview = ReturnType<typeof EmailPreview>;
7
+ export default EmailPreview;
@@ -0,0 +1,103 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ /**
3
+ * Import all Svelte email components file paths.
4
+ * Create a list containing all Svelte email component file names.
5
+ * Return this list to the client.
6
+ */
7
+ export type PreviewData = {
8
+ files: string[] | null;
9
+ path: string | null;
10
+ };
11
+ type EmailListProps = {
12
+ path?: string;
13
+ root?: string;
14
+ };
15
+ /**
16
+ * Get a list of all email component files in the specified directory.
17
+ *
18
+ * @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
19
+ * @param options.root - Absolute path to project root (auto-detected if not provided)
20
+ * @returns PreviewData object with list of email files and the path
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // In a +page.server.ts file
25
+ * import { emailList } from 'better-svelte-email/preview';
26
+ *
27
+ * export function load() {
28
+ * const emails = emailList({
29
+ * root: process.cwd(),
30
+ * path: '/src/lib/emails'
31
+ * });
32
+ * return { emails };
33
+ * }
34
+ * ```
35
+ */
36
+ export declare const emailList: ({ path: emailPath, root }?: EmailListProps) => PreviewData;
37
+ /**
38
+ * SvelteKit form action to render an email component.
39
+ * Use this with the Preview component to render email templates on demand.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * // +page.server.ts
44
+ * import { createEmail } from 'better-svelte-email/preview';
45
+ *
46
+ * export const actions = createEmail;
47
+ * ```
48
+ */
49
+ export declare const createEmail: {
50
+ 'create-email': (event: RequestEvent) => Promise<{
51
+ status: number;
52
+ body: {
53
+ error: string;
54
+ };
55
+ error?: undefined;
56
+ } | {
57
+ body: string;
58
+ status?: undefined;
59
+ error?: undefined;
60
+ } | {
61
+ status: number;
62
+ error: {
63
+ message: string;
64
+ };
65
+ body?: undefined;
66
+ }>;
67
+ };
68
+ export declare const SendEmailFunction: ({ from, to, subject, html }: {
69
+ from: string;
70
+ to: string;
71
+ subject: string;
72
+ html: string;
73
+ }, resendApiKey?: string) => Promise<{
74
+ success: boolean;
75
+ error?: any;
76
+ }>;
77
+ /**
78
+ * Sends the email using the submitted form data.
79
+ *
80
+ * @param options.resendApiKey - Your Resend API key (keep this server-side only)
81
+ * @param options.customSendEmailFunction - Optional custom function to send emails
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // In +page.server.ts
86
+ * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
87
+ *
88
+ * export const actions = {
89
+ * ...createEmail,
90
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
91
+ * };
92
+ * ```
93
+ */
94
+ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
95
+ customSendEmailFunction?: typeof SendEmailFunction;
96
+ resendApiKey?: string;
97
+ }) => {
98
+ 'send-email': (event: RequestEvent) => Promise<{
99
+ success: boolean;
100
+ error: any;
101
+ }>;
102
+ };
103
+ export { default as EmailPreview } from './EmailPreview.svelte';
@@ -0,0 +1,205 @@
1
+ import { Resend } from 'resend';
2
+ import fs from 'fs';
3
+ import { render } from 'svelte/server';
4
+ import path from 'path';
5
+ import prettier from 'prettier/standalone';
6
+ import parserHtml from 'prettier/parser-html';
7
+ /**
8
+ * Get a list of all email component files in the specified directory.
9
+ *
10
+ * @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
11
+ * @param options.root - Absolute path to project root (auto-detected if not provided)
12
+ * @returns PreviewData object with list of email files and the path
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // In a +page.server.ts file
17
+ * import { emailList } from 'better-svelte-email/preview';
18
+ *
19
+ * export function load() {
20
+ * const emails = emailList({
21
+ * root: process.cwd(),
22
+ * path: '/src/lib/emails'
23
+ * });
24
+ * return { emails };
25
+ * }
26
+ * ```
27
+ */
28
+ export const emailList = ({ path: emailPath = '/src/lib/emails', root } = {}) => {
29
+ // If root is not provided, try to use process.cwd()
30
+ if (!root) {
31
+ try {
32
+ root = process.cwd();
33
+ }
34
+ catch {
35
+ 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');
36
+ }
37
+ }
38
+ const fullPath = path.join(root, emailPath);
39
+ // Check if directory exists
40
+ if (!fs.existsSync(fullPath)) {
41
+ console.warn(`Email directory not found: ${fullPath}`);
42
+ return { files: null, path: emailPath };
43
+ }
44
+ const files = createEmailComponentList(emailPath, getFiles(fullPath));
45
+ if (!files.length) {
46
+ return { files: null, path: emailPath };
47
+ }
48
+ return { files, path: emailPath };
49
+ };
50
+ /**
51
+ * SvelteKit form action to render an email component.
52
+ * Use this with the Preview component to render email templates on demand.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * // +page.server.ts
57
+ * import { createEmail } from 'better-svelte-email/preview';
58
+ *
59
+ * export const actions = createEmail;
60
+ * ```
61
+ */
62
+ export const createEmail = {
63
+ 'create-email': async (event) => {
64
+ try {
65
+ const data = await event.request.formData();
66
+ const file = data.get('file');
67
+ const emailPath = data.get('path');
68
+ if (!file || !emailPath) {
69
+ return {
70
+ status: 400,
71
+ body: { error: 'Missing file or path parameter' }
72
+ };
73
+ }
74
+ const getEmailComponent = async () => {
75
+ try {
76
+ // Import the email component dynamically
77
+ return (await import(/* @vite-ignore */ `${emailPath}/${file}.svelte`)).default;
78
+ }
79
+ catch {
80
+ throw new Error(`Failed to import email component '${file}'. Make sure the file exists and includes the <Head /> component.`);
81
+ }
82
+ };
83
+ const emailComponent = await getEmailComponent();
84
+ // Render the component to HTML
85
+ const { body } = render(emailComponent);
86
+ // Remove all HTML comments from the body before formatting
87
+ const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
88
+ const formattedBody = await prettier.format(bodyWithoutComments, {
89
+ parser: 'html',
90
+ plugins: [parserHtml]
91
+ });
92
+ return {
93
+ body: formattedBody
94
+ };
95
+ }
96
+ catch (error) {
97
+ console.error('Error rendering email:', error);
98
+ return {
99
+ status: 500,
100
+ error: {
101
+ message: error instanceof Error ? error.message : 'Failed to render email'
102
+ }
103
+ };
104
+ }
105
+ }
106
+ };
107
+ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
108
+ // stringify api key to comment out temp
109
+ const resend = new Resend(resendApiKey);
110
+ const email = { from, to, subject, html };
111
+ const resendReq = await resend.emails.send(email);
112
+ if (resendReq.error) {
113
+ return { success: false, error: resendReq.error };
114
+ }
115
+ else {
116
+ return { success: true, error: null };
117
+ }
118
+ };
119
+ /**
120
+ * Sends the email using the submitted form data.
121
+ *
122
+ * @param options.resendApiKey - Your Resend API key (keep this server-side only)
123
+ * @param options.customSendEmailFunction - Optional custom function to send emails
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // In +page.server.ts
128
+ * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
129
+ *
130
+ * export const actions = {
131
+ * ...createEmail,
132
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
133
+ * };
134
+ * ```
135
+ */
136
+ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
137
+ return {
138
+ 'send-email': async (event) => {
139
+ const data = await event.request.formData();
140
+ const email = {
141
+ from: 'svelte-email-tailwind <onboarding@resend.dev>',
142
+ to: `${data.get('to')}`,
143
+ subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
144
+ html: `${data.get('html')}`
145
+ };
146
+ let sent = { success: false, error: null };
147
+ if (!customSendEmailFunction && resendApiKey) {
148
+ sent = await defaultSendEmailFunction(email, resendApiKey);
149
+ }
150
+ else if (customSendEmailFunction) {
151
+ sent = await customSendEmailFunction(email, resendApiKey);
152
+ }
153
+ else if (!customSendEmailFunction && !resendApiKey) {
154
+ const error = {
155
+ message: 'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
156
+ };
157
+ return { success: false, error };
158
+ }
159
+ if (sent && sent.error) {
160
+ console.log('Error:', sent.error);
161
+ return { success: false, error: sent.error };
162
+ }
163
+ else {
164
+ console.log('Email was sent successfully.');
165
+ return { success: true, error: null };
166
+ }
167
+ }
168
+ };
169
+ };
170
+ // Recursive function to get files
171
+ function getFiles(dir, files = []) {
172
+ // Get an array of all files and directories in the passed directory using fs.readdirSync
173
+ const fileList = fs.readdirSync(dir);
174
+ // Create the full path of the file/directory by concatenating the passed directory and file/directory name
175
+ for (const file of fileList) {
176
+ const name = `${dir}/${file}`;
177
+ // Check if the current file/directory is a directory using fs.statSync
178
+ if (fs.statSync(name).isDirectory()) {
179
+ // If it is a directory, recursively call the getFiles function with the directory path and the files array
180
+ getFiles(name, files);
181
+ }
182
+ else {
183
+ // If it is a file, push the full path to the files array
184
+ files.push(name);
185
+ }
186
+ }
187
+ return files;
188
+ }
189
+ /**
190
+ * Creates an array of names from the record of svelte email component file paths
191
+ */
192
+ function createEmailComponentList(root, paths) {
193
+ const emailComponentList = [];
194
+ paths.forEach((filePath) => {
195
+ if (filePath.includes(`.svelte`)) {
196
+ const fileName = filePath.substring(filePath.indexOf(root) + root.length + 1, filePath.indexOf('.svelte'));
197
+ emailComponentList.push(fileName);
198
+ }
199
+ });
200
+ return emailComponentList;
201
+ }
202
+ // Export the Preview component
203
+ // Note: The component is available via: import EmailPreview from 'better-svelte-email/preview/EmailPreview.svelte'
204
+ // or: import { EmailPreview } from 'better-svelte-email/preview'
205
+ export { default as EmailPreview } from './EmailPreview.svelte';
@@ -10,3 +10,30 @@ export declare function styleToString(style: Record<string, string | number | un
10
10
  * @returns Point value as string
11
11
  */
12
12
  export declare function pxToPt(px: string | number): string;
13
+ export type Margin = {
14
+ m?: string;
15
+ mx?: string;
16
+ my?: string;
17
+ mt?: string;
18
+ mr?: string;
19
+ mb?: string;
20
+ ml?: string;
21
+ };
22
+ /**
23
+ * Convert margin props to a CSS style object
24
+ * @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
25
+ * @returns Style object with margin properties in pixels
26
+ */
27
+ export declare function withMargin(props: Margin): any;
28
+ /**
29
+ * Combine multiple styles into a single string
30
+ * @param styles - Array of style strings
31
+ * @returns Combined style string
32
+ */
33
+ export declare function combineStyles(...styles: (string | undefined | null)[]): string;
34
+ /**
35
+ * Render HTML as plain text
36
+ * @param markup - HTML string
37
+ * @returns Plain text string
38
+ */
39
+ export declare const renderAsPlainText: (markup: string) => string;
@@ -1,3 +1,4 @@
1
+ import { convert } from 'html-to-text';
1
2
  /**
2
3
  * Convert a style object to a CSS string
3
4
  * @param style - Object containing CSS properties
@@ -22,3 +23,49 @@ export function pxToPt(px) {
22
23
  const value = typeof px === 'string' ? parseFloat(px) : px;
23
24
  return `${Math.round(value * 0.75)}pt`;
24
25
  }
26
+ /**
27
+ * Convert margin props to a CSS style object
28
+ * @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
29
+ * @returns Style object with margin properties in pixels
30
+ */
31
+ export function withMargin(props) {
32
+ const margins = [
33
+ withSpace(props.m, ['margin']),
34
+ withSpace(props.mx, ['marginLeft', 'marginRight']),
35
+ withSpace(props.my, ['marginTop', 'marginBottom']),
36
+ withSpace(props.mt, ['marginTop']),
37
+ withSpace(props.mr, ['marginRight']),
38
+ withSpace(props.mb, ['marginBottom']),
39
+ withSpace(props.ml, ['marginLeft'])
40
+ ];
41
+ return Object.assign({}, ...margins);
42
+ }
43
+ function withSpace(value, properties) {
44
+ return properties.reduce((styles, property) => {
45
+ if (value) {
46
+ return { ...styles, [property]: `${value}px` };
47
+ }
48
+ return styles;
49
+ }, {});
50
+ }
51
+ /**
52
+ * Combine multiple styles into a single string
53
+ * @param styles - Array of style strings
54
+ * @returns Combined style string
55
+ */
56
+ export function combineStyles(...styles) {
57
+ return styles.filter((style) => style !== '' && style !== undefined && style !== null).join(';');
58
+ }
59
+ /**
60
+ * Render HTML as plain text
61
+ * @param markup - HTML string
62
+ * @returns Plain text string
63
+ */
64
+ export const renderAsPlainText = (markup) => {
65
+ return convert(markup, {
66
+ selectors: [
67
+ { selector: 'img', format: 'skip' },
68
+ { selector: '#__better-svelte-email-preview', format: 'skip' }
69
+ ]
70
+ });
71
+ };
package/package.json CHANGED
@@ -1,26 +1,40 @@
1
1
  {
2
2
  "name": "better-svelte-email",
3
- "version": "0.1.0",
4
- "author": "Anatole",
3
+ "version": "0.3.0",
4
+ "author": "Konixy",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Konixy/better-svelte-email.git"
8
8
  },
9
9
  "peerDependencies": {
10
- "svelte": "^5.14.3"
10
+ "svelte": "^5.14.3",
11
+ "@sveltejs/kit": "^2.0.0"
12
+ },
13
+ "peerDependenciesMeta": {
14
+ "@sveltejs/kit": {
15
+ "optional": true
16
+ }
11
17
  },
12
18
  "dependencies": {
19
+ "html-to-text": "^9.0.5",
13
20
  "magic-string": "^0.30.19",
21
+ "svelte-highlight": "^7.8.4",
14
22
  "tw-to-css": "^0.0.12"
15
23
  },
24
+ "optionalDependencies": {
25
+ "prettier": "^3.6.2",
26
+ "resend": "^6.1.2"
27
+ },
16
28
  "devDependencies": {
17
29
  "@eslint/compat": "^1.4.0",
18
30
  "@eslint/js": "^9.37.0",
19
31
  "@sveltejs/adapter-auto": "^6.1.1",
32
+ "@sveltejs/adapter-vercel": "^5.10.3",
20
33
  "@sveltejs/kit": "^2.43.8",
21
34
  "@sveltejs/package": "^2.5.4",
22
35
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
23
36
  "@tailwindcss/vite": "^4.1.14",
37
+ "@types/html-to-text": "^9.0.4",
24
38
  "@types/node": "^24",
25
39
  "@vitest/browser": "^3.2.4",
26
40
  "eslint": "^9.37.0",
@@ -28,11 +42,9 @@
28
42
  "eslint-plugin-svelte": "^3.12.4",
29
43
  "globals": "^16.4.0",
30
44
  "playwright": "^1.55.1",
31
- "prettier": "^3.6.2",
32
45
  "prettier-plugin-svelte": "^3.4.0",
33
46
  "prettier-plugin-tailwindcss": "^0.6.14",
34
47
  "publint": "^0.3.13",
35
- "resend": "^6.1.2",
36
48
  "svelte": "^5.39.8",
37
49
  "svelte-check": "^4.3.2",
38
50
  "tailwindcss": "^4.1.14",
@@ -58,9 +70,23 @@
58
70
  "types": "./dist/components/*.svelte.d.ts",
59
71
  "svelte": "./dist/components/*.svelte"
60
72
  },
73
+ "./preview": {
74
+ "types": "./dist/preview/index.d.ts",
75
+ "import": "./dist/preview/index.js",
76
+ "default": "./dist/preview/index.js"
77
+ },
78
+ "./preview/EmailPreview.svelte": {
79
+ "types": "./dist/preview/EmailPreview.svelte.d.ts",
80
+ "svelte": "./dist/preview/EmailPreview.svelte"
81
+ },
82
+ "./utils": {
83
+ "types": "./dist/utils/index.d.ts",
84
+ "import": "./dist/utils/index.js",
85
+ "default": "./dist/utils/index.js"
86
+ },
61
87
  "./package.json": "./package.json"
62
88
  },
63
- "description": "A Svelte 5 preprocessor that transforms Tailwind CSS classes in email components to inline styles with responsive media query support",
89
+ "description": "Svelte email renderer with Tailwind support",
64
90
  "files": [
65
91
  "dist",
66
92
  "!dist/**/*.test.*",
@@ -81,8 +107,9 @@
81
107
  "license": "MIT",
82
108
  "scripts": {
83
109
  "dev": "vite dev",
84
- "build": "vite build && npm run prepack",
110
+ "build": "bun run prepack && vite build",
85
111
  "preview": "vite preview",
112
+ "package": "svelte-package",
86
113
  "prepare": "svelte-kit sync || echo ''",
87
114
  "prepack": "svelte-kit sync && svelte-package && publint",
88
115
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -1,13 +0,0 @@
1
- <script>
2
- import { Html, Head, Text, Button, Container } from '../index.js';
3
- </script>
4
-
5
- <Html>
6
- <Head />
7
- <Container class="bg-gray-100 p-8">
8
- <Text class="text-lg font-bold text-blue-600">Hello World</Text>
9
- <Button class="rounded bg-blue-500 px-4 py-2 text-white" href="https://example.com">
10
- Click Me
11
- </Button>
12
- </Container>
13
- </Html>