better-svelte-email 0.3.5 → 1.0.0-beta.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.
- package/dist/components/Body.svelte +7 -3
- package/dist/components/Button.svelte +10 -12
- package/dist/components/Button.svelte.d.ts +2 -2
- package/dist/index.d.ts +2 -6
- package/dist/index.js +3 -5
- package/dist/preprocessor/index.d.ts +19 -0
- package/dist/preprocessor/index.js +19 -0
- package/dist/preview/index.d.ts +34 -5
- package/dist/preview/index.js +91 -44
- package/dist/render/index.d.ts +66 -0
- package/dist/render/index.js +138 -0
- package/dist/render/utils/compatibility/sanitize-class-name.d.ts +7 -0
- package/dist/render/utils/compatibility/sanitize-class-name.js +35 -0
- package/dist/render/utils/css/extract-rules-per-class.d.ts +5 -0
- package/dist/render/utils/css/extract-rules-per-class.js +37 -0
- package/dist/render/utils/css/get-custom-properties.d.ts +8 -0
- package/dist/render/utils/css/get-custom-properties.js +37 -0
- package/dist/render/utils/css/is-rule-inlinable.d.ts +2 -0
- package/dist/render/utils/css/is-rule-inlinable.js +6 -0
- package/dist/render/utils/css/make-inline-styles-for.d.ts +3 -0
- package/dist/render/utils/css/make-inline-styles-for.js +57 -0
- package/dist/render/utils/css/resolve-all-css-variables.d.ts +8 -0
- package/dist/render/utils/css/resolve-all-css-variables.js +123 -0
- package/dist/render/utils/css/resolve-calc-expressions.d.ts +5 -0
- package/dist/render/utils/css/resolve-calc-expressions.js +126 -0
- package/dist/render/utils/css/sanitize-declarations.d.ts +15 -0
- package/dist/render/utils/css/sanitize-declarations.js +354 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.d.ts +11 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.js +33 -0
- package/dist/render/utils/css/sanitize-stylesheet.d.ts +2 -0
- package/dist/render/utils/css/sanitize-stylesheet.js +8 -0
- package/dist/render/utils/css/unwrap-value.d.ts +2 -0
- package/dist/render/utils/css/unwrap-value.js +6 -0
- package/dist/render/utils/html/is-valid-node.d.ts +2 -0
- package/dist/render/utils/html/is-valid-node.js +3 -0
- package/dist/render/utils/html/remove-attributes-functions.d.ts +2 -0
- package/dist/render/utils/html/remove-attributes-functions.js +10 -0
- package/dist/render/utils/html/walk.d.ts +15 -0
- package/dist/render/utils/html/walk.js +36 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.d.ts +4 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.js +61 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.d.ts +2 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.js +58 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.d.ts +7 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.js +67 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.js +899 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.js +396 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.js +465 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.js +4 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +13 -10
- package/package.json +17 -2
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
|
|
4
|
-
let {
|
|
5
|
-
|
|
4
|
+
let {
|
|
5
|
+
children,
|
|
6
|
+
style,
|
|
7
|
+
class: className,
|
|
8
|
+
...restProps
|
|
9
|
+
}: { children?: any } & HTMLAttributes<HTMLBodyElement> = $props();
|
|
6
10
|
</script>
|
|
7
11
|
|
|
8
12
|
<body {...restProps}>
|
|
9
13
|
<table align="center" width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
|
10
14
|
<tbody>
|
|
11
15
|
<tr>
|
|
12
|
-
<td {style}>
|
|
16
|
+
<td {style} class={className}>
|
|
13
17
|
{@render children?.()}
|
|
14
18
|
</td>
|
|
15
19
|
</tr>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { styleToString, pxToPt, combineStyles } from '../utils/index.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
4
4
|
|
|
5
5
|
let {
|
|
6
6
|
href = '#',
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
pX?: number;
|
|
17
17
|
pY?: number;
|
|
18
18
|
children: any;
|
|
19
|
-
} &
|
|
19
|
+
} & HTMLAnchorAttributes = $props();
|
|
20
20
|
|
|
21
21
|
const y = pY * 2;
|
|
22
22
|
const textRaise = pxToPt(y.toString());
|
|
@@ -43,17 +43,15 @@
|
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<a {...restProps} {href} {target} style={combineStyles(buttonStyle, style)}>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{/if}
|
|
46
|
+
<span>
|
|
47
|
+
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%;mso-text-raise:${textRaise}" hidden> </i><![endif]-->`}
|
|
48
|
+
</span>
|
|
49
|
+
|
|
51
50
|
<span style={buttonTextStyle}>
|
|
52
51
|
{@render children?.()}
|
|
53
52
|
</span>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{/if}
|
|
53
|
+
|
|
54
|
+
<span style="display: none;">
|
|
55
|
+
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%" hidden> </i><![endif]-->`}
|
|
56
|
+
</span>
|
|
59
57
|
</a>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
href?: string;
|
|
4
4
|
target?: string;
|
|
5
5
|
pX?: number;
|
|
6
6
|
pY?: number;
|
|
7
7
|
children: any;
|
|
8
|
-
} &
|
|
8
|
+
} & HTMLAnchorAttributes;
|
|
9
9
|
declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
10
|
type Button = ReturnType<typeof Button>;
|
|
11
11
|
export default Button;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export * from './components/index.js';
|
|
2
2
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
3
3
|
export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export { parseAttributes as parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
|
|
7
|
-
export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
|
|
8
|
-
export { injectMediaQueries } from './preprocessor/head-injector.js';
|
|
9
|
-
export { styleToString, pxToPt, combineStyles, withMargin, renderAsPlainText } from './utils/index.js';
|
|
4
|
+
export { default as Renderer, type TailwindConfig, type RenderOptions } from './render/index.js';
|
|
5
|
+
export * from './utils/index.js';
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
export * from './components/index.js';
|
|
3
3
|
// Export the preprocessor
|
|
4
4
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
5
|
-
// Export
|
|
6
|
-
export {
|
|
7
|
-
export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
|
|
8
|
-
export { injectMediaQueries } from './preprocessor/head-injector.js';
|
|
5
|
+
// Export renderer
|
|
6
|
+
export { default as Renderer } from './render/index.js';
|
|
9
7
|
// Export utilities
|
|
10
|
-
export
|
|
8
|
+
export * from './utils/index.js';
|
|
@@ -3,8 +3,11 @@ import type { PreprocessorOptions, ComponentTransform } from './types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
5
5
|
*
|
|
6
|
+
* @deprecated The preprocessor approach is deprecated. Use the `Renderer` class instead for better performance and flexibility.
|
|
7
|
+
*
|
|
6
8
|
* @example
|
|
7
9
|
* ```javascript
|
|
10
|
+
* // Old (deprecated):
|
|
8
11
|
* // svelte.config.js
|
|
9
12
|
* import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
|
|
10
13
|
*
|
|
@@ -17,6 +20,22 @@ import type { PreprocessorOptions, ComponentTransform } from './types.js';
|
|
|
17
20
|
* })
|
|
18
21
|
* ]
|
|
19
22
|
* };
|
|
23
|
+
*
|
|
24
|
+
* // New (recommended):
|
|
25
|
+
* import Renderer from 'better-svelte-email/renderer';
|
|
26
|
+
* import EmailComponent from './email.svelte';
|
|
27
|
+
*
|
|
28
|
+
* const renderer = new Renderer({
|
|
29
|
+
* theme: {
|
|
30
|
+
* extend: {
|
|
31
|
+
* colors: { brand: '#FF3E00' }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const html = await renderer.render(EmailComponent, {
|
|
37
|
+
* props: { name: 'John' }
|
|
38
|
+
* });
|
|
20
39
|
* ```
|
|
21
40
|
*
|
|
22
41
|
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
|
@@ -6,8 +6,11 @@ import path from 'path';
|
|
|
6
6
|
/**
|
|
7
7
|
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
8
8
|
*
|
|
9
|
+
* @deprecated The preprocessor approach is deprecated. Use the `Renderer` class instead for better performance and flexibility.
|
|
10
|
+
*
|
|
9
11
|
* @example
|
|
10
12
|
* ```javascript
|
|
13
|
+
* // Old (deprecated):
|
|
11
14
|
* // svelte.config.js
|
|
12
15
|
* import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
|
|
13
16
|
*
|
|
@@ -20,6 +23,22 @@ import path from 'path';
|
|
|
20
23
|
* })
|
|
21
24
|
* ]
|
|
22
25
|
* };
|
|
26
|
+
*
|
|
27
|
+
* // New (recommended):
|
|
28
|
+
* import Renderer from 'better-svelte-email/renderer';
|
|
29
|
+
* import EmailComponent from './email.svelte';
|
|
30
|
+
*
|
|
31
|
+
* const renderer = new Renderer({
|
|
32
|
+
* theme: {
|
|
33
|
+
* extend: {
|
|
34
|
+
* colors: { brand: '#FF3E00' }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* const html = await renderer.render(EmailComponent, {
|
|
40
|
+
* props: { name: 'John' }
|
|
41
|
+
* });
|
|
23
42
|
* ```
|
|
24
43
|
*
|
|
25
44
|
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
import Renderer from '../render/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Import all Svelte email components file paths.
|
|
4
5
|
* Create a list containing all Svelte email component file names.
|
|
@@ -34,19 +35,33 @@ type EmailListProps = {
|
|
|
34
35
|
* ```
|
|
35
36
|
*/
|
|
36
37
|
export declare const emailList: ({ path: emailPath, root }?: EmailListProps) => PreviewData;
|
|
38
|
+
export declare const getEmailComponent: (emailPath: string, file: string) => Promise<any>;
|
|
37
39
|
/**
|
|
38
40
|
* SvelteKit form action to render an email component.
|
|
39
41
|
* Use this with the Preview component to render email templates on demand.
|
|
40
42
|
*
|
|
43
|
+
* @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
|
|
44
|
+
*
|
|
41
45
|
* @example
|
|
42
46
|
* ```ts
|
|
43
47
|
* // +page.server.ts
|
|
44
48
|
* import { createEmail } from 'better-svelte-email/preview';
|
|
49
|
+
* import Renderer from 'better-svelte-email/render';
|
|
50
|
+
*
|
|
51
|
+
* const renderer = new Renderer({
|
|
52
|
+
* theme: {
|
|
53
|
+
* extend: {
|
|
54
|
+
* colors: {
|
|
55
|
+
* brand: '#FF3E00'
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
45
60
|
*
|
|
46
|
-
* export const actions = createEmail;
|
|
61
|
+
* export const actions = createEmail(renderer);
|
|
47
62
|
* ```
|
|
48
63
|
*/
|
|
49
|
-
export declare const createEmail: {
|
|
64
|
+
export declare const createEmail: (renderer?: Renderer) => {
|
|
50
65
|
'create-email': (event: RequestEvent) => Promise<{
|
|
51
66
|
status: number;
|
|
52
67
|
body: {
|
|
@@ -79,20 +94,33 @@ export declare const SendEmailFunction: ({ from, to, subject, html }: {
|
|
|
79
94
|
*
|
|
80
95
|
* @param options.resendApiKey - Your Resend API key (keep this server-side only)
|
|
81
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)
|
|
82
98
|
*
|
|
83
99
|
* @example
|
|
84
100
|
* ```ts
|
|
85
101
|
* // In +page.server.ts
|
|
86
102
|
* import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
|
|
103
|
+
* import Renderer from 'better-svelte-email/render';
|
|
104
|
+
*
|
|
105
|
+
* const renderer = new Renderer({
|
|
106
|
+
* theme: {
|
|
107
|
+
* extend: {
|
|
108
|
+
* colors: {
|
|
109
|
+
* brand: '#FF3E00'
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* }
|
|
113
|
+
* });
|
|
87
114
|
*
|
|
88
115
|
* export const actions = {
|
|
89
|
-
* ...createEmail,
|
|
90
|
-
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
|
|
116
|
+
* ...createEmail(renderer),
|
|
117
|
+
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
|
|
91
118
|
* };
|
|
92
119
|
* ```
|
|
93
120
|
*/
|
|
94
|
-
export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
|
|
121
|
+
export declare const sendEmail: ({ customSendEmailFunction, resendApiKey, renderer }?: {
|
|
95
122
|
customSendEmailFunction?: typeof SendEmailFunction;
|
|
123
|
+
renderer?: Renderer;
|
|
96
124
|
resendApiKey?: string;
|
|
97
125
|
}) => {
|
|
98
126
|
'send-email': (event: RequestEvent) => Promise<{
|
|
@@ -100,4 +128,5 @@ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
|
|
|
100
128
|
error: any;
|
|
101
129
|
}>;
|
|
102
130
|
};
|
|
131
|
+
export declare function getFiles(dir: string, files?: string[]): string[];
|
|
103
132
|
export { default as EmailPreview } from './EmailPreview.svelte';
|
package/dist/preview/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Resend } from 'resend';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { render } from 'svelte/server';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import prettier from 'prettier/standalone';
|
|
6
5
|
import parserHtml from 'prettier/parser-html';
|
|
6
|
+
import Renderer from '../render/index.js';
|
|
7
7
|
/**
|
|
8
8
|
* Get a list of all email component files in the specified directory.
|
|
9
9
|
*
|
|
@@ -42,17 +42,22 @@ export const emailList = ({ path: emailPath = '/src/lib/emails', root } = {}) =>
|
|
|
42
42
|
console.warn(`Email directory not found: ${fullPath}`);
|
|
43
43
|
return { files: null, path: emailPath };
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Use the absolute folder path as the root when creating the component list so
|
|
46
|
+
// we can compute correct relative paths on all platforms.
|
|
47
|
+
const files = createEmailComponentList(fullPath, getFiles(fullPath));
|
|
46
48
|
if (!files.length) {
|
|
47
49
|
return { files: null, path: emailPath };
|
|
48
50
|
}
|
|
49
51
|
return { files, path: emailPath };
|
|
50
52
|
};
|
|
51
|
-
const getEmailComponent = async (emailPath, file) => {
|
|
53
|
+
export const getEmailComponent = async (emailPath, file) => {
|
|
52
54
|
const fileName = `${file}.svelte`;
|
|
53
55
|
try {
|
|
54
56
|
// Import the email component dynamically
|
|
55
|
-
|
|
57
|
+
const normalizedEmailPath = emailPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
58
|
+
const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
59
|
+
const importPath = `${normalizedEmailPath}/${normalizedFile}.svelte`;
|
|
60
|
+
return (await import(/* @vite-ignore */ importPath)).default;
|
|
56
61
|
}
|
|
57
62
|
catch (err) {
|
|
58
63
|
throw new Error(`Failed to import email component '${fileName}'. Make sure the file exists and includes the <Head /> component.\nOriginal error: ${err}`);
|
|
@@ -62,49 +67,63 @@ const getEmailComponent = async (emailPath, file) => {
|
|
|
62
67
|
* SvelteKit form action to render an email component.
|
|
63
68
|
* Use this with the Preview component to render email templates on demand.
|
|
64
69
|
*
|
|
70
|
+
* @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
|
|
71
|
+
*
|
|
65
72
|
* @example
|
|
66
73
|
* ```ts
|
|
67
74
|
* // +page.server.ts
|
|
68
75
|
* import { createEmail } from 'better-svelte-email/preview';
|
|
76
|
+
* import Renderer from 'better-svelte-email/render';
|
|
77
|
+
*
|
|
78
|
+
* const renderer = new Renderer({
|
|
79
|
+
* theme: {
|
|
80
|
+
* extend: {
|
|
81
|
+
* colors: {
|
|
82
|
+
* brand: '#FF3E00'
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
* });
|
|
69
87
|
*
|
|
70
|
-
* export const actions = createEmail;
|
|
88
|
+
* export const actions = createEmail(renderer);
|
|
71
89
|
* ```
|
|
72
90
|
*/
|
|
73
|
-
export const createEmail = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
export const createEmail = (renderer = new Renderer()) => {
|
|
92
|
+
return {
|
|
93
|
+
'create-email': async (event) => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await event.request.formData();
|
|
96
|
+
const file = data.get('file');
|
|
97
|
+
const emailPath = data.get('path');
|
|
98
|
+
if (!file || !emailPath) {
|
|
99
|
+
return {
|
|
100
|
+
status: 400,
|
|
101
|
+
body: { error: 'Missing file or path parameter' }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const emailComponent = await getEmailComponent(emailPath, file);
|
|
105
|
+
// Render the component to HTML
|
|
106
|
+
const html = await renderer.render(emailComponent);
|
|
107
|
+
// Remove all HTML comments from the body before formatting
|
|
108
|
+
const formattedHtml = await prettier.format(html, {
|
|
109
|
+
parser: 'html',
|
|
110
|
+
plugins: [parserHtml]
|
|
111
|
+
});
|
|
80
112
|
return {
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
body: formattedHtml
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Error rendering email:', error);
|
|
118
|
+
return {
|
|
119
|
+
status: 500,
|
|
120
|
+
error: {
|
|
121
|
+
message: error instanceof Error ? error.message : 'Failed to render email'
|
|
122
|
+
}
|
|
83
123
|
};
|
|
84
124
|
}
|
|
85
|
-
const emailComponent = await getEmailComponent(emailPath, file);
|
|
86
|
-
// Render the component to HTML
|
|
87
|
-
const { body } = render(emailComponent);
|
|
88
|
-
// Remove all HTML comments from the body before formatting
|
|
89
|
-
const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
|
|
90
|
-
const formattedBody = await prettier.format(bodyWithoutComments, {
|
|
91
|
-
parser: 'html',
|
|
92
|
-
plugins: [parserHtml]
|
|
93
|
-
});
|
|
94
|
-
return {
|
|
95
|
-
body: formattedBody
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
console.error('Error rendering email:', error);
|
|
100
|
-
return {
|
|
101
|
-
status: 500,
|
|
102
|
-
error: {
|
|
103
|
-
message: error instanceof Error ? error.message : 'Failed to render email'
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
125
|
}
|
|
107
|
-
}
|
|
126
|
+
};
|
|
108
127
|
};
|
|
109
128
|
const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
|
|
110
129
|
// stringify api key to comment out temp
|
|
@@ -123,19 +142,31 @@ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKe
|
|
|
123
142
|
*
|
|
124
143
|
* @param options.resendApiKey - Your Resend API key (keep this server-side only)
|
|
125
144
|
* @param options.customSendEmailFunction - Optional custom function to send emails
|
|
145
|
+
* @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
|
|
126
146
|
*
|
|
127
147
|
* @example
|
|
128
148
|
* ```ts
|
|
129
149
|
* // In +page.server.ts
|
|
130
150
|
* import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
|
|
151
|
+
* import Renderer from 'better-svelte-email/render';
|
|
152
|
+
*
|
|
153
|
+
* const renderer = new Renderer({
|
|
154
|
+
* theme: {
|
|
155
|
+
* extend: {
|
|
156
|
+
* colors: {
|
|
157
|
+
* brand: '#FF3E00'
|
|
158
|
+
* }
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* });
|
|
131
162
|
*
|
|
132
163
|
* export const actions = {
|
|
133
|
-
* ...createEmail,
|
|
134
|
-
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
|
|
164
|
+
* ...createEmail(renderer),
|
|
165
|
+
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
|
|
135
166
|
* };
|
|
136
167
|
* ```
|
|
137
168
|
*/
|
|
138
|
-
export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
169
|
+
export const sendEmail = ({ customSendEmailFunction, resendApiKey, renderer = new Renderer() } = {}) => {
|
|
139
170
|
return {
|
|
140
171
|
'send-email': async (event) => {
|
|
141
172
|
const data = await event.request.formData();
|
|
@@ -152,7 +183,7 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
|
152
183
|
from: 'svelte-email-tailwind <onboarding@resend.dev>',
|
|
153
184
|
to: `${data.get('to')}`,
|
|
154
185
|
subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
|
|
155
|
-
html:
|
|
186
|
+
html: await renderer.render(emailComponent)
|
|
156
187
|
};
|
|
157
188
|
let sent = { success: false, error: null };
|
|
158
189
|
if (!customSendEmailFunction && resendApiKey) {
|
|
@@ -179,7 +210,7 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
|
179
210
|
};
|
|
180
211
|
};
|
|
181
212
|
// Recursive function to get files
|
|
182
|
-
function getFiles(dir, files = []) {
|
|
213
|
+
export function getFiles(dir, files = []) {
|
|
183
214
|
// Get an array of all files and directories in the passed directory using fs.readdirSync
|
|
184
215
|
const fileList = fs.readdirSync(dir);
|
|
185
216
|
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
|
|
@@ -203,9 +234,25 @@ function getFiles(dir, files = []) {
|
|
|
203
234
|
function createEmailComponentList(root, paths) {
|
|
204
235
|
const emailComponentList = [];
|
|
205
236
|
paths.forEach((filePath) => {
|
|
206
|
-
if (filePath.
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
if (filePath.endsWith('.svelte')) {
|
|
238
|
+
// Get the directory name from the full path
|
|
239
|
+
const fileDir = path.dirname(filePath);
|
|
240
|
+
// Get the base name without extension
|
|
241
|
+
const baseName = path.basename(filePath, '.svelte');
|
|
242
|
+
// Normalize paths for cross-platform comparison
|
|
243
|
+
const rootNormalized = path.normalize(root);
|
|
244
|
+
const fileDirNormalized = path.normalize(fileDir);
|
|
245
|
+
// Find where root appears in the full directory path
|
|
246
|
+
const rootIndex = fileDirNormalized.indexOf(rootNormalized);
|
|
247
|
+
if (rootIndex !== -1) {
|
|
248
|
+
// Get everything after the root path
|
|
249
|
+
const afterRoot = fileDirNormalized.substring(rootIndex + rootNormalized.length);
|
|
250
|
+
// Combine with the base name using path.join for proper separators
|
|
251
|
+
const relativePath = afterRoot ? path.join(afterRoot, baseName) : baseName;
|
|
252
|
+
// Remove leading path separators
|
|
253
|
+
const cleanPath = relativePath.replace(/^[/\\]+/, '');
|
|
254
|
+
emailComponentList.push(cleanPath);
|
|
255
|
+
}
|
|
209
256
|
}
|
|
210
257
|
});
|
|
211
258
|
return emailComponentList;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type DefaultTreeAdapterTypes } from 'parse5';
|
|
2
|
+
import type { Config } from 'tailwindcss';
|
|
3
|
+
export type TailwindConfig = Omit<Config, 'content'>;
|
|
4
|
+
export type { DefaultTreeAdapterTypes as AST };
|
|
5
|
+
/**
|
|
6
|
+
* Options for rendering a Svelte component
|
|
7
|
+
*/
|
|
8
|
+
export type RenderOptions = {
|
|
9
|
+
props?: Omit<Record<string, any>, '$$slots' | '$$events'> | undefined;
|
|
10
|
+
context?: Map<any, any>;
|
|
11
|
+
idPrefix?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Email renderer that converts Svelte components to email-safe HTML with inlined Tailwind styles.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import Renderer from 'better-svelte-email/renderer';
|
|
19
|
+
* import EmailComponent from './email.svelte';
|
|
20
|
+
*
|
|
21
|
+
* const renderer = new Renderer({
|
|
22
|
+
* theme: {
|
|
23
|
+
* extend: {
|
|
24
|
+
* colors: {
|
|
25
|
+
* brand: '#FF3E00'
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const html = await renderer.render(EmailComponent, {
|
|
32
|
+
* props: { name: 'John' }
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export default class Renderer {
|
|
37
|
+
private tailwindConfig;
|
|
38
|
+
constructor(tailwindConfig?: TailwindConfig);
|
|
39
|
+
/**
|
|
40
|
+
* Renders a Svelte component to email-safe HTML with inlined Tailwind CSS.
|
|
41
|
+
*
|
|
42
|
+
* Automatically:
|
|
43
|
+
* - Converts Tailwind classes to inline styles
|
|
44
|
+
* - Injects media queries into `<head>` for responsive classes
|
|
45
|
+
* - Replaces DOCTYPE with XHTML 1.0 Transitional
|
|
46
|
+
* - Removes comments and Svelte artifacts
|
|
47
|
+
*
|
|
48
|
+
* @param component - The Svelte component to render
|
|
49
|
+
* @param options - Render options including props, context, and idPrefix
|
|
50
|
+
* @returns Email-safe HTML string
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const html = await renderer.render(EmailComponent, {
|
|
55
|
+
* props: { username: 'john_doe', resetUrl: 'https://...' }
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
render: (component: any, options?: RenderOptions | undefined) => Promise<string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Render HTML as plain text
|
|
63
|
+
* @param markup - HTML string
|
|
64
|
+
* @returns Plain text string
|
|
65
|
+
*/
|
|
66
|
+
export declare const toPlainText: (markup: string) => string;
|