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.
Files changed (38) hide show
  1. package/README.md +422 -0
  2. package/dist/components/Body.svelte +9 -0
  3. package/dist/components/Body.svelte.d.ts +13 -0
  4. package/dist/components/Button.svelte +54 -0
  5. package/dist/components/Button.svelte.d.ts +21 -0
  6. package/dist/components/Container.svelte +28 -0
  7. package/dist/components/Container.svelte.d.ts +13 -0
  8. package/dist/components/Head.svelte +13 -0
  9. package/dist/components/Head.svelte.d.ts +6 -0
  10. package/dist/components/Html.svelte +19 -0
  11. package/dist/components/Html.svelte.d.ts +10 -0
  12. package/dist/components/Section.svelte +21 -0
  13. package/dist/components/Section.svelte.d.ts +13 -0
  14. package/dist/components/Text.svelte +17 -0
  15. package/dist/components/Text.svelte.d.ts +15 -0
  16. package/dist/components/__tests__/test-email.svelte +13 -0
  17. package/dist/components/__tests__/test-email.svelte.d.ts +26 -0
  18. package/dist/components/index.d.ts +7 -0
  19. package/dist/components/index.js +9 -0
  20. package/dist/emails/demo-email.svelte +108 -0
  21. package/dist/emails/demo-email.svelte.d.ts +13 -0
  22. package/dist/emails/test-email.svelte +15 -0
  23. package/dist/emails/test-email.svelte.d.ts +26 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +10 -0
  26. package/dist/preprocessor/head-injector.d.ts +9 -0
  27. package/dist/preprocessor/head-injector.js +57 -0
  28. package/dist/preprocessor/index.d.ts +25 -0
  29. package/dist/preprocessor/index.js +196 -0
  30. package/dist/preprocessor/parser.d.ts +14 -0
  31. package/dist/preprocessor/parser.js +249 -0
  32. package/dist/preprocessor/transformer.d.ts +18 -0
  33. package/dist/preprocessor/transformer.js +158 -0
  34. package/dist/preprocessor/types.d.ts +104 -0
  35. package/dist/preprocessor/types.js +1 -0
  36. package/dist/utils/index.d.ts +12 -0
  37. package/dist/utils/index.js +24 -0
  38. package/package.json +97 -0
@@ -0,0 +1,249 @@
1
+ import { parse } from 'svelte/compiler';
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 function parseClassAttributes(source) {
7
+ const classAttributes = [];
8
+ try {
9
+ // Parse the Svelte file into an AST
10
+ // Svelte 5 parse returns a Root node with modern AST structure
11
+ const ast = parse(source);
12
+ // Walk the html fragment (template portion) of the AST
13
+ if (ast.html && ast.html.children) {
14
+ for (const child of ast.html.children) {
15
+ walkNode(child, classAttributes, source);
16
+ }
17
+ }
18
+ }
19
+ catch (error) {
20
+ console.error('Failed to parse Svelte file:', error);
21
+ throw error;
22
+ }
23
+ return classAttributes;
24
+ }
25
+ /**
26
+ * Recursively walk Svelte 5 AST nodes to find class attributes
27
+ */
28
+ function walkNode(node, classAttributes, source) {
29
+ if (!node)
30
+ return;
31
+ // Svelte 5 AST structure:
32
+ // - Element: HTML elements like <div>, <button>
33
+ // - InlineComponent: Custom components like <Button>, <Head>
34
+ // - SlotElement: <svelte:element> and other svelte: elements
35
+ if (node.type === 'Element' ||
36
+ node.type === 'InlineComponent' ||
37
+ node.type === 'SlotElement' ||
38
+ node.type === 'Component') {
39
+ const elementName = node.name || 'unknown';
40
+ // Look for class attribute in Svelte 5 AST
41
+ const classAttr = node.attributes?.find((attr) => attr.type === 'Attribute' && attr.name === 'class');
42
+ if (classAttr && classAttr.value) {
43
+ // Extract class value
44
+ const extracted = extractClassValue(classAttr, source);
45
+ if (extracted) {
46
+ classAttributes.push({
47
+ raw: extracted.value,
48
+ start: extracted.start,
49
+ end: extracted.end,
50
+ elementName,
51
+ isStatic: extracted.isStatic
52
+ });
53
+ }
54
+ }
55
+ }
56
+ // Recursively process children
57
+ if (node.children) {
58
+ for (const child of node.children) {
59
+ walkNode(child, classAttributes, source);
60
+ }
61
+ }
62
+ // Handle conditional blocks (#if, #each, etc.)
63
+ if (node.consequent) {
64
+ if (node.consequent.children) {
65
+ for (const child of node.consequent.children) {
66
+ walkNode(child, classAttributes, source);
67
+ }
68
+ }
69
+ }
70
+ if (node.alternate) {
71
+ if (node.alternate.children) {
72
+ for (const child of node.alternate.children) {
73
+ walkNode(child, classAttributes, source);
74
+ }
75
+ }
76
+ }
77
+ // Handle #each blocks
78
+ if (node.body) {
79
+ if (node.body.children) {
80
+ for (const child of node.body.children) {
81
+ walkNode(child, classAttributes, source);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Extract the actual class value from a Svelte 5 attribute node
88
+ */
89
+ function extractClassValue(classAttr, source) {
90
+ // Svelte 5 attribute value formats:
91
+ // 1. Static string: class="text-red-500"
92
+ // → value: [{ type: 'Text', data: 'text-red-500' }]
93
+ //
94
+ // 2. Expression: class={someVar}
95
+ // → value: [{ type: 'ExpressionTag', expression: {...} }]
96
+ //
97
+ // 3. Mixed: class="static {dynamic} more"
98
+ // → value: [{ type: 'Text' }, { type: 'ExpressionTag' }, { type: 'Text' }]
99
+ if (!classAttr.value || classAttr.value.length === 0) {
100
+ return null;
101
+ }
102
+ // Check if entirely static (only Text nodes)
103
+ const hasOnlyText = classAttr.value.every((v) => v.type === 'Text');
104
+ if (hasOnlyText) {
105
+ // Fully static - we can safely transform this
106
+ const textContent = classAttr.value.map((v) => v.data || '').join('');
107
+ const start = classAttr.value[0].start;
108
+ const end = classAttr.value[classAttr.value.length - 1].end;
109
+ return {
110
+ value: textContent,
111
+ start,
112
+ end,
113
+ isStatic: true
114
+ };
115
+ }
116
+ // Check if entirely dynamic (only ExpressionTag or MustacheTag)
117
+ const hasOnlyExpression = classAttr.value.length === 1 &&
118
+ (classAttr.value[0].type === 'ExpressionTag' || classAttr.value[0].type === 'MustacheTag');
119
+ if (hasOnlyExpression) {
120
+ // Fully dynamic - cannot transform at build time
121
+ const exprNode = classAttr.value[0];
122
+ const expressionCode = source.substring(exprNode.start, exprNode.end);
123
+ return {
124
+ value: expressionCode,
125
+ start: exprNode.start,
126
+ end: exprNode.end,
127
+ isStatic: false
128
+ };
129
+ }
130
+ // Mixed content (both Text and ExpressionTag)
131
+ // Extract only the static Text portions for partial transformation
132
+ let combinedValue = '';
133
+ let start = classAttr.value[0].start;
134
+ let end = classAttr.value[classAttr.value.length - 1].end;
135
+ let hasStaticContent = false;
136
+ for (const part of classAttr.value) {
137
+ if (part.type === 'Text' && part.data) {
138
+ combinedValue += part.data + ' ';
139
+ hasStaticContent = true;
140
+ }
141
+ // Skip ExpressionTag nodes
142
+ }
143
+ if (hasStaticContent) {
144
+ return {
145
+ value: combinedValue.trim(),
146
+ start,
147
+ end,
148
+ isStatic: false // Mixed is not fully static
149
+ };
150
+ }
151
+ return null;
152
+ }
153
+ /**
154
+ * Find the <Head> component in Svelte 5 AST
155
+ * Returns the position where we should inject styles
156
+ */
157
+ export function findHeadComponent(source) {
158
+ try {
159
+ const ast = parse(source);
160
+ // Find Head component in the AST
161
+ if (ast.html && ast.html.children) {
162
+ for (const child of ast.html.children) {
163
+ const headInfo = findHeadInNode(child, source);
164
+ if (headInfo)
165
+ return headInfo;
166
+ }
167
+ }
168
+ return { found: false, insertPosition: null };
169
+ }
170
+ catch (error) {
171
+ return { found: false, insertPosition: null };
172
+ }
173
+ }
174
+ /**
175
+ * Recursively search for Head component in Svelte 5 AST
176
+ */
177
+ function findHeadInNode(node, source) {
178
+ if (!node)
179
+ return null;
180
+ // Check if this is the Head component (InlineComponent type in Svelte 5)
181
+ if ((node.type === 'InlineComponent' || node.type === 'Component') && node.name === 'Head') {
182
+ // Svelte 5: Find the best insertion point for styles
183
+ // If Head has children, insert before first child
184
+ if (node.children && node.children.length > 0) {
185
+ return {
186
+ found: true,
187
+ insertPosition: node.children[0].start
188
+ };
189
+ }
190
+ // No children - need to insert before closing tag
191
+ // Find where the opening tag ends
192
+ const headStart = node.start;
193
+ const headEnd = node.end;
194
+ const headContent = source.substring(headStart, headEnd);
195
+ // Self-closing: <Head />
196
+ if (headContent.includes('/>')) {
197
+ // Convert to non-self-closing by inserting before />
198
+ const selfClosingPos = source.indexOf('/>', headStart);
199
+ return {
200
+ found: true,
201
+ insertPosition: selfClosingPos
202
+ };
203
+ }
204
+ // Regular closing tag: <Head></Head> or <Head>...</Head>
205
+ const closingTagPos = source.indexOf('</Head>', headStart);
206
+ if (closingTagPos !== -1) {
207
+ return {
208
+ found: true,
209
+ insertPosition: closingTagPos
210
+ };
211
+ }
212
+ // Fallback: insert right after opening tag
213
+ const openingTagEnd = source.indexOf('>', headStart);
214
+ if (openingTagEnd !== -1) {
215
+ return {
216
+ found: true,
217
+ insertPosition: openingTagEnd + 1
218
+ };
219
+ }
220
+ }
221
+ // Search recursively through the AST
222
+ if (node.children) {
223
+ for (const child of node.children) {
224
+ const found = findHeadInNode(child, source);
225
+ if (found)
226
+ return found;
227
+ }
228
+ }
229
+ // Check conditional branches
230
+ if (node.consequent) {
231
+ if (node.consequent.children) {
232
+ for (const child of node.consequent.children) {
233
+ const found = findHeadInNode(child, source);
234
+ if (found)
235
+ return found;
236
+ }
237
+ }
238
+ }
239
+ if (node.alternate) {
240
+ if (node.alternate.children) {
241
+ for (const child of node.alternate.children) {
242
+ const found = findHeadInNode(child, source);
243
+ if (found)
244
+ return found;
245
+ }
246
+ }
247
+ }
248
+ return null;
249
+ }
@@ -0,0 +1,18 @@
1
+ import { type TailwindConfig } from 'tw-to-css';
2
+ import type { TransformResult, MediaQueryStyle } from './types.js';
3
+ /**
4
+ * Initialize Tailwind converter with config
5
+ */
6
+ export declare function createTailwindConverter(config?: TailwindConfig): typeof import("tw-to-css").twi;
7
+ /**
8
+ * Transform Tailwind classes to inline styles and responsive classes
9
+ */
10
+ export declare function transformTailwindClasses(classString: string, tailwindConverter: ReturnType<typeof createTailwindConverter>): TransformResult;
11
+ /**
12
+ * Generate media query CSS for responsive classes
13
+ */
14
+ export declare function generateMediaQueries(responsiveClasses: string[], tailwindConverter: ReturnType<typeof createTailwindConverter>, tailwindConfig?: TailwindConfig): MediaQueryStyle[];
15
+ /**
16
+ * Sanitize class names for use in CSS (replace special characters)
17
+ */
18
+ export declare function sanitizeClassName(className: string): string;
@@ -0,0 +1,158 @@
1
+ import { tailwindToCSS } from 'tw-to-css';
2
+ /**
3
+ * Initialize Tailwind converter with config
4
+ */
5
+ export function createTailwindConverter(config) {
6
+ const { twi } = tailwindToCSS({ config });
7
+ return twi;
8
+ }
9
+ /**
10
+ * Transform Tailwind classes to inline styles and responsive classes
11
+ */
12
+ export function transformTailwindClasses(classString, tailwindConverter) {
13
+ // Split classes
14
+ const classes = classString.trim().split(/\s+/).filter(Boolean);
15
+ // Separate responsive from non-responsive classes
16
+ const responsiveClasses = [];
17
+ const nonResponsiveClasses = [];
18
+ for (const cls of classes) {
19
+ // Responsive classes have format: sm:, md:, lg:, xl:, 2xl:
20
+ if (/^(sm|md|lg|xl|2xl):/.test(cls)) {
21
+ responsiveClasses.push(cls);
22
+ }
23
+ else {
24
+ nonResponsiveClasses.push(cls);
25
+ }
26
+ }
27
+ // Convert non-responsive classes to CSS
28
+ let inlineStyles = '';
29
+ const invalidClasses = [];
30
+ if (nonResponsiveClasses.length > 0) {
31
+ const classesStr = nonResponsiveClasses.join(' ');
32
+ try {
33
+ // Generate CSS from Tailwind classes
34
+ const css = tailwindConverter(classesStr, {
35
+ merge: false,
36
+ ignoreMediaQueries: true
37
+ });
38
+ // Extract styles from CSS
39
+ const styles = extractStylesFromCSS(css, nonResponsiveClasses);
40
+ inlineStyles = styles.validStyles;
41
+ invalidClasses.push(...styles.invalidClasses);
42
+ }
43
+ catch (error) {
44
+ console.warn('Failed to convert Tailwind classes:', error);
45
+ }
46
+ }
47
+ return {
48
+ inlineStyles,
49
+ responsiveClasses,
50
+ invalidClasses
51
+ };
52
+ }
53
+ /**
54
+ * Extract CSS properties from generated CSS
55
+ * Handles the format: .classname { prop: value; }
56
+ */
57
+ function extractStylesFromCSS(css, originalClasses) {
58
+ const invalidClasses = [];
59
+ const styleProperties = [];
60
+ // Remove media queries (we handle those separately)
61
+ const cssWithoutMedia = css.replace(/@media[^{]+\{(?:[^{}]|\{[^{}]*\})*\}/g, '');
62
+ // Create a map of class name -> CSS rules
63
+ const classMap = new Map();
64
+ // Match .classname { rules }
65
+ const classRegex = /\.([^\s{]+)\s*\{([^}]+)\}/g;
66
+ let match;
67
+ while ((match = classRegex.exec(cssWithoutMedia)) !== null) {
68
+ const className = match[1];
69
+ const rules = match[2].trim();
70
+ // Normalize class name (tw-to-css might transform special chars)
71
+ const normalizedClass = className.replace(/[:#\-[\]/.%!_]+/g, '_');
72
+ classMap.set(normalizedClass, rules);
73
+ }
74
+ // For each original class, try to find its CSS
75
+ for (const originalClass of originalClasses) {
76
+ // Normalize the original class name to match what tw-to-css produces
77
+ const normalized = originalClass.replace(/[:#\-[\]/.%!]+/g, '_');
78
+ if (classMap.has(normalized)) {
79
+ const rules = classMap.get(normalized);
80
+ // Ensure rules end with semicolon for proper concatenation
81
+ const rulesWithSemicolon = rules.trim().endsWith(';') ? rules.trim() : rules.trim() + ';';
82
+ styleProperties.push(rulesWithSemicolon);
83
+ }
84
+ else {
85
+ // Class not found - might be invalid Tailwind
86
+ invalidClasses.push(originalClass);
87
+ }
88
+ }
89
+ // Combine all style properties with space separator
90
+ const validStyles = styleProperties.join(' ').trim();
91
+ return { validStyles, invalidClasses };
92
+ }
93
+ /**
94
+ * Generate media query CSS for responsive classes
95
+ */
96
+ export function generateMediaQueries(responsiveClasses, tailwindConverter, tailwindConfig) {
97
+ if (responsiveClasses.length === 0) {
98
+ return [];
99
+ }
100
+ const mediaQueries = [];
101
+ // Default breakpoints (can be overridden by config)
102
+ const breakpoints = {
103
+ sm: '475px',
104
+ md: '768px',
105
+ lg: '1024px',
106
+ xl: '1280px',
107
+ '2xl': '1536px',
108
+ ...tailwindConfig?.theme?.screens
109
+ };
110
+ // Group classes by breakpoint
111
+ const classesByBreakpoint = new Map();
112
+ for (const cls of responsiveClasses) {
113
+ const match = cls.match(/^(sm|md|lg|xl|2xl):(.+)/);
114
+ if (match) {
115
+ const [, breakpoint] = match;
116
+ if (!classesByBreakpoint.has(breakpoint)) {
117
+ classesByBreakpoint.set(breakpoint, []);
118
+ }
119
+ classesByBreakpoint.get(breakpoint).push(cls);
120
+ }
121
+ }
122
+ // Generate CSS for each breakpoint
123
+ for (const [breakpoint, classes] of classesByBreakpoint) {
124
+ const breakpointValue = breakpoints[breakpoint];
125
+ if (!breakpointValue)
126
+ continue;
127
+ // Generate full CSS including media queries
128
+ const fullCSS = tailwindConverter(classes.join(' '), {
129
+ merge: false,
130
+ ignoreMediaQueries: false
131
+ });
132
+ // Extract just the media query portion
133
+ const mediaQueryRegex = new RegExp(`@media[^{]*\\{([^{}]|\\{[^{}]*\\})*\\}`, 'g');
134
+ let match;
135
+ while ((match = mediaQueryRegex.exec(fullCSS)) !== null) {
136
+ const mediaQueryBlock = match[0];
137
+ // Make all rules !important for email clients
138
+ const withImportant = mediaQueryBlock.replace(/([a-z-]+)\s*:\s*([^;!}]+)/gi, '$1: $2 !important');
139
+ // Parse out the query and content
140
+ const queryMatch = withImportant.match(/@media\s*([^{]+)/);
141
+ if (queryMatch) {
142
+ const query = `@media ${queryMatch[1].trim()}`;
143
+ mediaQueries.push({
144
+ query,
145
+ className: `responsive-${breakpoint}`,
146
+ rules: withImportant
147
+ });
148
+ }
149
+ }
150
+ }
151
+ return mediaQueries;
152
+ }
153
+ /**
154
+ * Sanitize class names for use in CSS (replace special characters)
155
+ */
156
+ export function sanitizeClassName(className) {
157
+ return className.replace(/[:#\-[\]/.%!]+/g, '_');
158
+ }
@@ -0,0 +1,104 @@
1
+ import type { TailwindConfig } from 'tw-to-css';
2
+ /**
3
+ * Options for the preprocessor
4
+ */
5
+ export interface PreprocessorOptions {
6
+ /**
7
+ * Custom Tailwind configuration
8
+ */
9
+ tailwindConfig?: TailwindConfig;
10
+ /**
11
+ * Path to folder containing email components
12
+ * @default '/src/lib/emails'
13
+ */
14
+ pathToEmailFolder?: string;
15
+ /**
16
+ * Enable debug logging
17
+ * @default false
18
+ */
19
+ debug?: boolean;
20
+ }
21
+ /**
22
+ * Represents a class attribute found in the AST
23
+ */
24
+ export interface ClassAttribute {
25
+ /**
26
+ * Raw class string (e.g., "text-red-500 sm:bg-blue")
27
+ */
28
+ raw: string;
29
+ /**
30
+ * Start position in source code
31
+ */
32
+ start: number;
33
+ /**
34
+ * End position in source code
35
+ */
36
+ end: number;
37
+ /**
38
+ * Parent element/component name
39
+ */
40
+ elementName: string;
41
+ /**
42
+ * Whether this is a static string or dynamic expression
43
+ */
44
+ isStatic: boolean;
45
+ }
46
+ /**
47
+ * Result of transforming Tailwind classes
48
+ */
49
+ export interface TransformResult {
50
+ /**
51
+ * CSS styles for inline styleString prop
52
+ */
53
+ inlineStyles: string;
54
+ /**
55
+ * Responsive classes to keep in class attribute
56
+ */
57
+ responsiveClasses: string[];
58
+ /**
59
+ * Classes that couldn't be converted (warnings)
60
+ */
61
+ invalidClasses: string[];
62
+ }
63
+ /**
64
+ * Media query CSS to inject into head
65
+ */
66
+ export interface MediaQueryStyle {
67
+ /**
68
+ * Media query condition (e.g., "@media (max-width: 475px)")
69
+ */
70
+ query: string;
71
+ /**
72
+ * CSS class name
73
+ */
74
+ className: string;
75
+ /**
76
+ * CSS rules
77
+ */
78
+ rules: string;
79
+ }
80
+ /**
81
+ * Information about a component's transformations
82
+ */
83
+ export interface ComponentTransform {
84
+ /**
85
+ * Original source code
86
+ */
87
+ originalCode: string;
88
+ /**
89
+ * Transformed source code
90
+ */
91
+ transformedCode: string;
92
+ /**
93
+ * Media queries to inject
94
+ */
95
+ mediaQueries: MediaQueryStyle[];
96
+ /**
97
+ * Whether <Head> component was found
98
+ */
99
+ hasHead: boolean;
100
+ /**
101
+ * Warnings encountered during transformation
102
+ */
103
+ warnings: string[];
104
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert a style object to a CSS string
3
+ * @param style - Object containing CSS properties
4
+ * @returns CSS string with properties
5
+ */
6
+ export declare function styleToString(style: Record<string, string | number | undefined>): string;
7
+ /**
8
+ * Convert pixels to points for email clients
9
+ * @param px - Pixel value as string
10
+ * @returns Point value as string
11
+ */
12
+ export declare function pxToPt(px: string | number): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Convert a style object to a CSS string
3
+ * @param style - Object containing CSS properties
4
+ * @returns CSS string with properties
5
+ */
6
+ export function styleToString(style) {
7
+ return Object.entries(style)
8
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
9
+ .map(([key, value]) => {
10
+ // Convert camelCase to kebab-case
11
+ const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
12
+ return `${cssKey}:${value}`;
13
+ })
14
+ .join(';');
15
+ }
16
+ /**
17
+ * Convert pixels to points for email clients
18
+ * @param px - Pixel value as string
19
+ * @returns Point value as string
20
+ */
21
+ export function pxToPt(px) {
22
+ const value = typeof px === 'string' ? parseFloat(px) : px;
23
+ return `${Math.round(value * 0.75)}pt`;
24
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "better-svelte-email",
3
+ "version": "0.0.1",
4
+ "description": "A Svelte 5 preprocessor that transforms Tailwind CSS classes in email components to inline styles with responsive media query support",
5
+ "author": "Anatole",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Konixy/better-svelte-email.git"
10
+ },
11
+ "scripts": {
12
+ "dev": "vite dev",
13
+ "build": "vite build && npm run prepack",
14
+ "preview": "vite preview",
15
+ "prepare": "svelte-kit sync || echo ''",
16
+ "prepack": "svelte-kit sync && svelte-package && publint",
17
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
18
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
19
+ "format": "prettier --write .",
20
+ "lint": "prettier --check . && eslint .",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:ui": "vitest --ui",
24
+ "test:coverage": "vitest run --coverage"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "!dist/**/*.test.*",
29
+ "!dist/**/*.spec.*"
30
+ ],
31
+ "sideEffects": [
32
+ "**/*.css"
33
+ ],
34
+ "svelte": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "type": "module",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "svelte": "./dist/index.js",
41
+ "default": "./dist/index.js"
42
+ },
43
+ "./preprocessor": {
44
+ "types": "./dist/preprocessor/index.d.ts",
45
+ "import": "./dist/preprocessor/index.js",
46
+ "default": "./dist/preprocessor/index.js"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "svelte": "^5.0.0"
51
+ },
52
+ "dependencies": {
53
+ "magic-string": "^0.30.19",
54
+ "resend": "^6.1.2",
55
+ "tw-to-css": "^0.0.12"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/compat": "^1.4.0",
59
+ "@eslint/js": "^9.37.0",
60
+ "@sveltejs/adapter-auto": "^6.1.1",
61
+ "@sveltejs/kit": "^2.43.8",
62
+ "@sveltejs/package": "^2.5.4",
63
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
64
+ "@tailwindcss/vite": "^4.1.14",
65
+ "@types/node": "^24",
66
+ "@vitest/browser": "^3.2.4",
67
+ "eslint": "^9.37.0",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "eslint-plugin-svelte": "^3.12.4",
70
+ "globals": "^16.4.0",
71
+ "playwright": "^1.55.1",
72
+ "prettier": "^3.6.2",
73
+ "prettier-plugin-svelte": "^3.4.0",
74
+ "prettier-plugin-tailwindcss": "^0.6.14",
75
+ "publint": "^0.3.13",
76
+ "svelte": "^5.39.8",
77
+ "svelte-check": "^4.3.2",
78
+ "tailwindcss": "^4.1.14",
79
+ "typescript": "^5.9.3",
80
+ "typescript-eslint": "^8.45.0",
81
+ "vite": "^7.1.9",
82
+ "vitest": "^3.2.4",
83
+ "vitest-browser-svelte": "^1.1.0"
84
+ },
85
+ "keywords": [
86
+ "svelte",
87
+ "svelte5",
88
+ "email",
89
+ "tailwind",
90
+ "tailwindcss",
91
+ "preprocessor",
92
+ "inline-styles",
93
+ "responsive-email",
94
+ "email-templates",
95
+ "svelte-preprocessor"
96
+ ]
97
+ }