better-svelte-email 0.0.1
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/README.md +422 -0
- package/dist/components/Body.svelte +9 -0
- package/dist/components/Body.svelte.d.ts +13 -0
- package/dist/components/Button.svelte +54 -0
- package/dist/components/Button.svelte.d.ts +21 -0
- package/dist/components/Container.svelte +28 -0
- package/dist/components/Container.svelte.d.ts +13 -0
- package/dist/components/Head.svelte +13 -0
- package/dist/components/Head.svelte.d.ts +6 -0
- package/dist/components/Html.svelte +19 -0
- package/dist/components/Html.svelte.d.ts +10 -0
- package/dist/components/Section.svelte +21 -0
- package/dist/components/Section.svelte.d.ts +13 -0
- package/dist/components/Text.svelte +17 -0
- package/dist/components/Text.svelte.d.ts +15 -0
- package/dist/components/__tests__/test-email.svelte +13 -0
- package/dist/components/__tests__/test-email.svelte.d.ts +26 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +9 -0
- package/dist/emails/demo-email.svelte +108 -0
- package/dist/emails/demo-email.svelte.d.ts +13 -0
- package/dist/emails/test-email.svelte +15 -0
- package/dist/emails/test-email.svelte.d.ts +26 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/preprocessor/head-injector.d.ts +9 -0
- package/dist/preprocessor/head-injector.js +57 -0
- package/dist/preprocessor/index.d.ts +25 -0
- package/dist/preprocessor/index.js +196 -0
- package/dist/preprocessor/parser.d.ts +14 -0
- package/dist/preprocessor/parser.js +249 -0
- package/dist/preprocessor/transformer.d.ts +18 -0
- package/dist/preprocessor/transformer.js +158 -0
- package/dist/preprocessor/types.d.ts +104 -0
- package/dist/preprocessor/types.js +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.js +24 -0
- package/package.json +97 -0
|
@@ -0,0 +1,13 @@
|
|
|
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>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default TestEmail;
|
|
2
|
+
type TestEmail = SvelteComponent<{
|
|
3
|
+
[x: string]: never;
|
|
4
|
+
}, {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
}, {}> & {
|
|
7
|
+
$$bindings?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
|
|
10
|
+
[x: string]: never;
|
|
11
|
+
}, {
|
|
12
|
+
[evt: string]: CustomEvent<any>;
|
|
13
|
+
}, {}, {}, string>;
|
|
14
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
15
|
+
new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
|
|
16
|
+
$$bindings?: Bindings;
|
|
17
|
+
} & Exports;
|
|
18
|
+
(internal: unknown, props: {
|
|
19
|
+
$$events?: Events;
|
|
20
|
+
$$slots?: Slots;
|
|
21
|
+
}): Exports & {
|
|
22
|
+
$set?: any;
|
|
23
|
+
$on?: any;
|
|
24
|
+
};
|
|
25
|
+
z_$$bindings?: Bindings;
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Html } from './Html.svelte';
|
|
2
|
+
export { default as Head } from './Head.svelte';
|
|
3
|
+
export { default as Body } from './Body.svelte';
|
|
4
|
+
export { default as Container } from './Container.svelte';
|
|
5
|
+
export { default as Section } from './Section.svelte';
|
|
6
|
+
export { default as Text } from './Text.svelte';
|
|
7
|
+
export { default as Button } from './Button.svelte';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Email Components for better-svelte-email
|
|
2
|
+
// These components work with the preprocessor's styleString prop
|
|
3
|
+
export { default as Html } from './Html.svelte';
|
|
4
|
+
export { default as Head } from './Head.svelte';
|
|
5
|
+
export { default as Body } from './Body.svelte';
|
|
6
|
+
export { default as Container } from './Container.svelte';
|
|
7
|
+
export { default as Section } from './Section.svelte';
|
|
8
|
+
export { default as Text } from './Text.svelte';
|
|
9
|
+
export { default as Button } from './Button.svelte';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Html, Head, Body, Container, Section, Text, Button } from '../components/index.js';
|
|
3
|
+
|
|
4
|
+
let { userName = 'User', testMessage = 'This is a test email!' } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<Html>
|
|
8
|
+
<Head>
|
|
9
|
+
<style>
|
|
10
|
+
@media(min-width: 640px){.sm\:bg-green-600{background-color:rgb(22,163,74) !important}}
|
|
11
|
+
</style>
|
|
12
|
+
</Head>
|
|
13
|
+
<Body styleString="background-color:rgb(243,244,246); font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";">
|
|
14
|
+
<Container styleString="margin-left:auto;margin-right:auto; max-width:42rem; background-color:rgb(255,255,255); padding:2rem;">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<Section styleString="margin-bottom:1.5rem; border-bottom-width:1px; border-color:rgb(229,231,235); padding-bottom:1.5rem;">
|
|
17
|
+
<Text as="h1" styleString="margin-bottom:0.5rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(17,24,39);">better-svelte-email Demo</Text>
|
|
18
|
+
<Text styleString="color:rgb(75,85,99);">
|
|
19
|
+
This email was generated using Tailwind classes and transformed by the preprocessor!
|
|
20
|
+
</Text>
|
|
21
|
+
</Section>
|
|
22
|
+
|
|
23
|
+
<!-- Main Content -->
|
|
24
|
+
<Section styleString="margin-bottom:1.5rem;">
|
|
25
|
+
<Text styleString="margin-bottom:1rem; font-size:1.125rem;line-height:1.75rem; color:rgb(31,41,55);">Hello {userName}! 👋</Text>
|
|
26
|
+
|
|
27
|
+
<Text styleString="margin-bottom:1rem; color:rgb(55,65,81);">{testMessage}</Text>
|
|
28
|
+
|
|
29
|
+
<Container styleString="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(239,246,255); padding:1rem;">
|
|
30
|
+
<Text styleString="margin-bottom:0.5rem; font-weight:600; color:rgb(30,58,138);">✨ Features Demonstrated:</Text>
|
|
31
|
+
<ul styleString="list-style-type:disc; padding-left:1.25rem; color:rgb(30,64,175);">
|
|
32
|
+
<li>Tailwind classes converted to inline styles</li>
|
|
33
|
+
<li>Responsive design with media queries</li>
|
|
34
|
+
<li>Email-safe CSS transformations</li>
|
|
35
|
+
<li>Build-time preprocessing (zero runtime cost)</li>
|
|
36
|
+
</ul>
|
|
37
|
+
</Container>
|
|
38
|
+
|
|
39
|
+
<!-- Buttons showcase -->
|
|
40
|
+
<Section styleString="margin-bottom:1.5rem;">
|
|
41
|
+
<Button
|
|
42
|
+
href="https://github.com/Konixy/better-svelte-email"
|
|
43
|
+
class="sm_bg_green_600" styleString="display:inline-block; border-radius:0.25rem; background-color:rgb(37,99,235); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(255,255,255);"
|
|
44
|
+
>
|
|
45
|
+
View on GitHub
|
|
46
|
+
</Button>
|
|
47
|
+
|
|
48
|
+
<Button
|
|
49
|
+
href="https://svelte.dev"
|
|
50
|
+
styleString="margin-left:0.75rem; display:inline-block; border-radius:0.25rem; border-width:1px; border-color:rgb(209,213,219); background-color:rgb(255,255,255); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(55,65,81);"
|
|
51
|
+
>
|
|
52
|
+
Learn Svelte 5
|
|
53
|
+
</Button>
|
|
54
|
+
</Section>
|
|
55
|
+
</Section>
|
|
56
|
+
|
|
57
|
+
<!-- Stats Grid -->
|
|
58
|
+
<Section styleString="margin-bottom:1.5rem;">
|
|
59
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
60
|
+
<tbody>
|
|
61
|
+
<tr>
|
|
62
|
+
<td styleString="border-radius:0.5rem; background-color:rgb(250,245,255); padding:1rem; text-align:center;" style="width: 33.33%;">
|
|
63
|
+
<Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(147,51,234);">52+</Text>
|
|
64
|
+
<Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(126,34,206);">Tests Passing</Text>
|
|
65
|
+
</td>
|
|
66
|
+
<td style="width: 10px;"></td>
|
|
67
|
+
<td styleString="border-radius:0.5rem; background-color:rgb(240,253,244); padding:1rem; text-align:center;" style="width: 33.33%;">
|
|
68
|
+
<Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(22,163,74);">100%</Text>
|
|
69
|
+
<Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(21,128,61);">TypeScript</Text>
|
|
70
|
+
</td>
|
|
71
|
+
<td style="width: 10px;"></td>
|
|
72
|
+
<td styleString="border-radius:0.5rem; background-color:rgb(255,247,237); padding:1rem; text-align:center;" style="width: 33.33%;">
|
|
73
|
+
<Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(234,88,12);">0ms</Text>
|
|
74
|
+
<Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(194,65,12);">Runtime Cost</Text>
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</Section>
|
|
80
|
+
|
|
81
|
+
<!-- Code Example -->
|
|
82
|
+
<Container styleString="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(249,250,251); padding:1rem;">
|
|
83
|
+
<Text styleString="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; font-weight:600; color:rgb(55,65,81);">How it works:</Text>
|
|
84
|
+
<pre
|
|
85
|
+
styleString="overflow-x:auto; border-radius:0.25rem; background-color:rgb(17,24,39); padding:0.75rem; font-size:0.75rem;line-height:1rem; color:rgb(243,244,246);"
|
|
86
|
+
style="margin: 0;"><code
|
|
87
|
+
>// Input
|
|
88
|
+
<Button class="bg-blue-500 text-white p-4">
|
|
89
|
+
Click Me
|
|
90
|
+
</Button>
|
|
91
|
+
|
|
92
|
+
// Output (after preprocessing)
|
|
93
|
+
<Button styleString="background-color: rgb(59, 130, 246); ...">
|
|
94
|
+
Click Me
|
|
95
|
+
</Button></code
|
|
96
|
+
></pre>
|
|
97
|
+
</Container>
|
|
98
|
+
|
|
99
|
+
<!-- Footer -->
|
|
100
|
+
<Section styleString="border-top-width:1px; border-color:rgb(229,231,235); padding-top:1.5rem; text-align:center;">
|
|
101
|
+
<Text styleString="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; color:rgb(75,85,99);">Built with Svelte 5 & Tailwind CSS</Text>
|
|
102
|
+
<Text styleString="font-size:0.75rem;line-height:1rem; color:rgb(107,114,128);">
|
|
103
|
+
You're receiving this email because you tested the better-svelte-email demo.
|
|
104
|
+
</Text>
|
|
105
|
+
</Section>
|
|
106
|
+
</Container>
|
|
107
|
+
</Body>
|
|
108
|
+
</Html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default DemoEmail;
|
|
2
|
+
type DemoEmail = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const DemoEmail: import("svelte").Component<{
|
|
7
|
+
userName?: string;
|
|
8
|
+
testMessage?: string;
|
|
9
|
+
}, {}, "">;
|
|
10
|
+
type $$ComponentProps = {
|
|
11
|
+
userName?: string;
|
|
12
|
+
testMessage?: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Html, Head, Body, Text, Button, Container } from '../components/index.js';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<Html>
|
|
6
|
+
<Head />
|
|
7
|
+
<Body>
|
|
8
|
+
<Container styleString="background-color:rgb(243,244,246); padding:2rem;">
|
|
9
|
+
<Text styleString="font-size:1.125rem;line-height:1.75rem; font-weight:700; color:rgb(37,99,235);">Hello World</Text>
|
|
10
|
+
<Button styleString="border-radius:0.25rem; background-color:rgb(59,130,246); padding-left:1rem;padding-right:1rem; padding-top:0.5rem;padding-bottom:0.5rem; color:rgb(255,255,255);" href="https://example.com">
|
|
11
|
+
Click Me
|
|
12
|
+
</Button>
|
|
13
|
+
</Container>
|
|
14
|
+
</Body>
|
|
15
|
+
</Html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default TestEmail;
|
|
2
|
+
type TestEmail = SvelteComponent<{
|
|
3
|
+
[x: string]: never;
|
|
4
|
+
}, {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
}, {}> & {
|
|
7
|
+
$$bindings?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
|
|
10
|
+
[x: string]: never;
|
|
11
|
+
}, {
|
|
12
|
+
[evt: string]: CustomEvent<any>;
|
|
13
|
+
}, {}, {}, string>;
|
|
14
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
15
|
+
new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
|
|
16
|
+
$$bindings?: Bindings;
|
|
17
|
+
} & Exports;
|
|
18
|
+
(internal: unknown, props: {
|
|
19
|
+
$$events?: Events;
|
|
20
|
+
$$slots?: Slots;
|
|
21
|
+
}): Exports & {
|
|
22
|
+
$set?: any;
|
|
23
|
+
$on?: any;
|
|
24
|
+
};
|
|
25
|
+
z_$$bindings?: Bindings;
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Html, Head, Body, Container, Section, Text, Button } from './components/index.js';
|
|
2
|
+
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
3
|
+
export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
|
|
4
|
+
export type { ClassAttribute, TransformResult, MediaQueryStyle } from './preprocessor/types.js';
|
|
5
|
+
export { parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
|
|
6
|
+
export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
|
|
7
|
+
export { injectMediaQueries } from './preprocessor/head-injector.js';
|
|
8
|
+
export { styleToString, pxToPt } from './utils/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Export email components
|
|
2
|
+
export { Html, Head, Body, Container, Section, Text, Button } from './components/index.js';
|
|
3
|
+
// Export the preprocessor
|
|
4
|
+
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
5
|
+
// Export individual functions for advanced usage
|
|
6
|
+
export { 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 utilities
|
|
10
|
+
export { styleToString, pxToPt } from './utils/index.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MediaQueryStyle } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inject media query styles into the <Head> component
|
|
4
|
+
*/
|
|
5
|
+
export declare function injectMediaQueries(source: string, mediaQueries: MediaQueryStyle[]): {
|
|
6
|
+
code: string;
|
|
7
|
+
success: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
import { findHeadComponent } from './parser.js';
|
|
3
|
+
/**
|
|
4
|
+
* Inject media query styles into the <Head> component
|
|
5
|
+
*/
|
|
6
|
+
export function injectMediaQueries(source, mediaQueries) {
|
|
7
|
+
if (mediaQueries.length === 0) {
|
|
8
|
+
// No media queries to inject
|
|
9
|
+
return { code: source, success: true };
|
|
10
|
+
}
|
|
11
|
+
// Find the Head component
|
|
12
|
+
const headInfo = findHeadComponent(source);
|
|
13
|
+
if (!headInfo.found || headInfo.insertPosition === null) {
|
|
14
|
+
return {
|
|
15
|
+
code: source,
|
|
16
|
+
success: false,
|
|
17
|
+
error: 'No <Head> component found. Media queries cannot be injected.'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Generate the style tag content
|
|
21
|
+
const styleContent = generateStyleTag(mediaQueries);
|
|
22
|
+
// Use MagicString for surgical insertion
|
|
23
|
+
const s = new MagicString(source);
|
|
24
|
+
// Check if Head is self-closing and convert it
|
|
25
|
+
const headStart = source.lastIndexOf('<Head', headInfo.insertPosition);
|
|
26
|
+
const headSegment = source.substring(headStart, headInfo.insertPosition + 10);
|
|
27
|
+
if (headSegment.includes('/>')) {
|
|
28
|
+
// Self-closing: convert to non-self-closing
|
|
29
|
+
// Check if there's a space before />
|
|
30
|
+
const spaceBeforeSelfClose = source[headInfo.insertPosition - 1] === ' ';
|
|
31
|
+
const replaceStart = spaceBeforeSelfClose
|
|
32
|
+
? headInfo.insertPosition - 1
|
|
33
|
+
: headInfo.insertPosition;
|
|
34
|
+
// Replace [space]?/> with >
|
|
35
|
+
s.overwrite(replaceStart, headInfo.insertPosition + 2, '>');
|
|
36
|
+
// Insert style content
|
|
37
|
+
s.appendLeft(headInfo.insertPosition + 2, styleContent);
|
|
38
|
+
// Add closing tag
|
|
39
|
+
s.appendLeft(headInfo.insertPosition + 2, '</Head>');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Already has closing tag, just insert content
|
|
43
|
+
s.appendLeft(headInfo.insertPosition, styleContent);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
code: s.toString(),
|
|
47
|
+
success: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generate <style> tag with all media queries
|
|
52
|
+
*/
|
|
53
|
+
function generateStyleTag(mediaQueries) {
|
|
54
|
+
// Combine all media queries
|
|
55
|
+
const allQueries = mediaQueries.map((mq) => mq.rules).join('\n');
|
|
56
|
+
return `\n\t<style>\n\t\t${allQueries}\n\t</style>\n`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PreprocessorGroup } from 'svelte/compiler';
|
|
2
|
+
import type { PreprocessorOptions, ComponentTransform } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```javascript
|
|
8
|
+
* // svelte.config.js
|
|
9
|
+
* import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
|
|
10
|
+
*
|
|
11
|
+
* export default {
|
|
12
|
+
* preprocess: [
|
|
13
|
+
* vitePreprocess(),
|
|
14
|
+
* betterSvelteEmailPreprocessor({
|
|
15
|
+
* pathToEmailFolder: '/src/lib/emails',
|
|
16
|
+
* tailwindConfig: { ... }
|
|
17
|
+
* })
|
|
18
|
+
* ]
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
|
23
|
+
*/
|
|
24
|
+
export declare function betterSvelteEmailPreprocessor(options?: PreprocessorOptions): PreprocessorGroup;
|
|
25
|
+
export type { PreprocessorOptions, ComponentTransform };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
import { parseClassAttributes } from './parser.js';
|
|
3
|
+
import { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './transformer.js';
|
|
4
|
+
import { injectMediaQueries } from './head-injector.js';
|
|
5
|
+
/**
|
|
6
|
+
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```javascript
|
|
10
|
+
* // svelte.config.js
|
|
11
|
+
* import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* preprocess: [
|
|
15
|
+
* vitePreprocess(),
|
|
16
|
+
* betterSvelteEmailPreprocessor({
|
|
17
|
+
* pathToEmailFolder: '/src/lib/emails',
|
|
18
|
+
* tailwindConfig: { ... }
|
|
19
|
+
* })
|
|
20
|
+
* ]
|
|
21
|
+
* };
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
|
25
|
+
*/
|
|
26
|
+
export function betterSvelteEmailPreprocessor(options = {}) {
|
|
27
|
+
const { tailwindConfig, pathToEmailFolder = '/src/lib/emails', debug = false } = options;
|
|
28
|
+
// Initialize Tailwind converter once (performance optimization)
|
|
29
|
+
const tailwindConverter = createTailwindConverter(tailwindConfig);
|
|
30
|
+
// Return a Svelte 5 PreprocessorGroup
|
|
31
|
+
return {
|
|
32
|
+
name: 'better-svelte-email',
|
|
33
|
+
/**
|
|
34
|
+
* The markup preprocessor transforms the template/HTML portion
|
|
35
|
+
* This is where we extract and transform Tailwind classes
|
|
36
|
+
*/
|
|
37
|
+
markup({ content, filename }) {
|
|
38
|
+
// Only process .svelte files in the configured email folder
|
|
39
|
+
if (!filename || !filename.includes(pathToEmailFolder)) {
|
|
40
|
+
// Return undefined to skip processing
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!filename.endsWith('.svelte')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
// Process the email component
|
|
48
|
+
const result = processEmailComponent(content, filename, tailwindConverter, tailwindConfig);
|
|
49
|
+
// Log warnings if debug mode is enabled
|
|
50
|
+
if (result.warnings.length > 0) {
|
|
51
|
+
if (debug) {
|
|
52
|
+
console.warn(`[better-svelte-email] Warnings for ${filename}:`, result.warnings);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Return the transformed code
|
|
56
|
+
// The preprocessor API expects { code: string } or { code: string, map: SourceMap }
|
|
57
|
+
return {
|
|
58
|
+
code: result.transformedCode
|
|
59
|
+
// Note: Source maps could be added here via MagicString's generateMap()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`[better-svelte-email] Error processing ${filename}:`, error);
|
|
64
|
+
// On error, return undefined to use original content
|
|
65
|
+
// This prevents breaking the build for non-email files
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Process a single email component
|
|
73
|
+
*/
|
|
74
|
+
function processEmailComponent(source, _filename, tailwindConverter, tailwindConfig) {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
let transformedCode = source;
|
|
77
|
+
const allMediaQueries = [];
|
|
78
|
+
// Step 1: Parse and find all class attributes
|
|
79
|
+
const classAttributes = parseClassAttributes(source);
|
|
80
|
+
if (classAttributes.length === 0) {
|
|
81
|
+
// No classes to transform
|
|
82
|
+
return {
|
|
83
|
+
originalCode: source,
|
|
84
|
+
transformedCode: source,
|
|
85
|
+
mediaQueries: [],
|
|
86
|
+
hasHead: false,
|
|
87
|
+
warnings: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Step 2: Transform each class attribute
|
|
91
|
+
const s = new MagicString(transformedCode);
|
|
92
|
+
// Process in reverse order to maintain correct positions
|
|
93
|
+
const sortedAttributes = [...classAttributes].sort((a, b) => b.start - a.start);
|
|
94
|
+
for (const classAttr of sortedAttributes) {
|
|
95
|
+
if (!classAttr.isStatic) {
|
|
96
|
+
// Skip dynamic classes for now
|
|
97
|
+
warnings.push(`Dynamic class expression detected in ${classAttr.elementName}. ` +
|
|
98
|
+
`Only static classes can be transformed at build time.`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Transform the classes
|
|
102
|
+
const transformed = transformTailwindClasses(classAttr.raw, tailwindConverter);
|
|
103
|
+
// Collect warnings about invalid classes
|
|
104
|
+
if (transformed.invalidClasses.length > 0) {
|
|
105
|
+
warnings.push(`Invalid Tailwind classes in ${classAttr.elementName}: ${transformed.invalidClasses.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
// Generate media queries for responsive classes
|
|
108
|
+
if (transformed.responsiveClasses.length > 0) {
|
|
109
|
+
const mediaQueries = generateMediaQueries(transformed.responsiveClasses, tailwindConverter, tailwindConfig);
|
|
110
|
+
allMediaQueries.push(...mediaQueries);
|
|
111
|
+
}
|
|
112
|
+
// Build the new attribute value
|
|
113
|
+
const newAttributes = buildNewAttributes(transformed.inlineStyles, transformed.responsiveClasses);
|
|
114
|
+
// Replace the class attribute with new attributes
|
|
115
|
+
replaceClassAttribute(s, classAttr, newAttributes);
|
|
116
|
+
}
|
|
117
|
+
transformedCode = s.toString();
|
|
118
|
+
// Step 3: Inject media queries into <Head>
|
|
119
|
+
if (allMediaQueries.length > 0) {
|
|
120
|
+
const injectionResult = injectMediaQueries(transformedCode, allMediaQueries);
|
|
121
|
+
if (!injectionResult.success) {
|
|
122
|
+
warnings.push(injectionResult.error || 'Failed to inject media queries');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
transformedCode = injectionResult.code;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
originalCode: source,
|
|
130
|
+
transformedCode,
|
|
131
|
+
mediaQueries: allMediaQueries,
|
|
132
|
+
hasHead: allMediaQueries.length > 0,
|
|
133
|
+
warnings
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build new attribute string from transformation result
|
|
138
|
+
*/
|
|
139
|
+
function buildNewAttributes(inlineStyles, responsiveClasses) {
|
|
140
|
+
const parts = [];
|
|
141
|
+
// Add responsive classes if any
|
|
142
|
+
if (responsiveClasses.length > 0) {
|
|
143
|
+
const sanitizedClasses = responsiveClasses.map(sanitizeClassName);
|
|
144
|
+
parts.push(`class="${sanitizedClasses.join(' ')}"`);
|
|
145
|
+
}
|
|
146
|
+
// Add inline styles if any
|
|
147
|
+
if (inlineStyles) {
|
|
148
|
+
// Escape quotes in styles
|
|
149
|
+
const escapedStyles = inlineStyles.replace(/"/g, '"');
|
|
150
|
+
parts.push(`styleString="${escapedStyles}"`);
|
|
151
|
+
}
|
|
152
|
+
return parts.join(' ');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Replace class attribute with new attributes using MagicString
|
|
156
|
+
*/
|
|
157
|
+
function replaceClassAttribute(s, classAttr, newAttributes) {
|
|
158
|
+
// We need to replace the entire class="..." portion
|
|
159
|
+
// The positions from AST are for the value, not the attribute
|
|
160
|
+
// So we need to search backwards for class="
|
|
161
|
+
// Find the start of the attribute (look for class=")
|
|
162
|
+
const beforeAttr = s.original.substring(0, classAttr.start);
|
|
163
|
+
const attrStartMatch = beforeAttr.lastIndexOf('class="');
|
|
164
|
+
if (attrStartMatch === -1) {
|
|
165
|
+
console.warn('Could not find class attribute start position');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Find the end of the attribute (closing quote)
|
|
169
|
+
const afterValue = s.original.substring(classAttr.end);
|
|
170
|
+
const quotePos = afterValue.indexOf('"');
|
|
171
|
+
if (quotePos === -1) {
|
|
172
|
+
console.warn('Could not find class attribute end position');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const fullAttrStart = attrStartMatch;
|
|
176
|
+
const fullAttrEnd = classAttr.end + quotePos + 1;
|
|
177
|
+
// Replace the entire class="..." with our new attributes
|
|
178
|
+
if (newAttributes) {
|
|
179
|
+
s.overwrite(fullAttrStart, fullAttrEnd, newAttributes);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// No attributes to add - remove the class attribute entirely
|
|
183
|
+
// Also remove any extra whitespace
|
|
184
|
+
let removeStart = fullAttrStart;
|
|
185
|
+
let removeEnd = fullAttrEnd;
|
|
186
|
+
// Check if there's a space before
|
|
187
|
+
if (s.original[removeStart - 1] === ' ') {
|
|
188
|
+
removeStart--;
|
|
189
|
+
}
|
|
190
|
+
// Check if there's a space after
|
|
191
|
+
if (s.original[removeEnd] === ' ') {
|
|
192
|
+
removeEnd++;
|
|
193
|
+
}
|
|
194
|
+
s.remove(removeStart, removeEnd);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClassAttribute } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse Svelte 5 source code and extract all class attributes
|
|
4
|
+
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#parse
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseClassAttributes(source: string): ClassAttribute[];
|
|
7
|
+
/**
|
|
8
|
+
* Find the <Head> component in Svelte 5 AST
|
|
9
|
+
* Returns the position where we should inject styles
|
|
10
|
+
*/
|
|
11
|
+
export declare function findHeadComponent(source: string): {
|
|
12
|
+
found: boolean;
|
|
13
|
+
insertPosition: number | null;
|
|
14
|
+
};
|