Skip to content

Commit 3cddec8

Browse files
committed
fix(preview): normalize component & import paths on Windows
1 parent d2129fe commit 3cddec8

File tree

1 file changed

+158
-152
lines changed

1 file changed

+158
-152
lines changed

src/lib/preview/index.ts

Lines changed: 158 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import parserHtml from 'prettier/parser-html';
1212
* Return this list to the client.
1313
*/
1414
export type PreviewData = {
15-
files: string[] | null;
16-
path: string | null;
15+
files: string[] | null;
16+
path: string | null;
1717
};
1818

1919
type EmailListProps = {
20-
path?: string;
21-
root?: string;
20+
path?: string;
21+
root?: string;
2222
};
2323

2424
/**
@@ -43,48 +43,54 @@ type EmailListProps = {
4343
* ```
4444
*/
4545
export const emailList = ({
46-
path: emailPath = '/src/lib/emails',
47-
root
46+
path: emailPath = '/src/lib/emails',
47+
root
4848
}: EmailListProps = {}): PreviewData => {
49-
// If root is not provided, try to use process.cwd()
50-
if (!root) {
51-
try {
52-
root = process.cwd();
53-
} catch (err) {
54-
throw new Error(
55-
'Could not determine the root path of your project. Please pass in the root param manually using process.cwd() or an absolute path.\nOriginal error: ' +
56-
err
57-
);
58-
}
59-
}
49+
// If root is not provided, try to use process.cwd()
50+
if (!root) {
51+
try {
52+
root = process.cwd();
53+
} catch (err) {
54+
throw new Error(
55+
'Could not determine the root path of your project. Please pass in the root param manually using process.cwd() or an absolute path.\nOriginal error: ' +
56+
err
57+
);
58+
}
59+
}
6060

61-
const fullPath = path.join(root, emailPath);
61+
const fullPath = path.join(root, emailPath);
6262

63-
// Check if directory exists
64-
if (!fs.existsSync(fullPath)) {
65-
console.warn(`Email directory not found: ${fullPath}`);
66-
return { files: null, path: emailPath };
67-
}
63+
// Check if directory exists
64+
if (!fs.existsSync(fullPath)) {
65+
console.warn(`Email directory not found: ${fullPath}`);
66+
return { files: null, path: emailPath };
67+
}
6868

69-
const files = createEmailComponentList(emailPath, getFiles(fullPath));
69+
// Use the absolute folder path as the root when creating the component list so
70+
// we can compute correct relative paths on all platforms.
71+
const files = createEmailComponentList(fullPath, getFiles(fullPath));
7072

71-
if (!files.length) {
72-
return { files: null, path: emailPath };
73-
}
73+
if (!files.length) {
74+
return { files: null, path: emailPath };
75+
}
7476

75-
return { files, path: emailPath };
77+
return { files, path: emailPath };
7678
};
7779

7880
const getEmailComponent = async (emailPath: string, file: string) => {
79-
const fileName = `${file}.svelte`;
80-
try {
81-
// Import the email component dynamically
82-
return (await import(/* @vite-ignore */ path.join(emailPath, fileName))).default;
83-
} catch (err) {
84-
throw new Error(
85-
`Failed to import email component '${fileName}'. Make sure the file exists and includes the <Head /> component.\nOriginal error: ${err}`
86-
);
87-
}
81+
const fileName = `${file}.svelte`;
82+
try {
83+
const normalizedEmailPath = emailPath.replace(/\\/g, '/').replace(/\/+$/, '');
84+
const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
85+
const importPath = `${normalizedEmailPath}/${normalizedFile}.svelte`;
86+
87+
// Import the email component dynamically
88+
return (await import(/* @vite-ignore */ importPath)).default;
89+
} catch (err) {
90+
throw new Error(
91+
`Failed to import email component '${fileName}'. Make sure the file exists and includes the <Head /> component.\nOriginal error: ${err}`
92+
);
93+
}
8894
};
8995

9096
/**
@@ -100,65 +106,65 @@ const getEmailComponent = async (emailPath: string, file: string) => {
100106
* ```
101107
*/
102108
export const createEmail = {
103-
'create-email': async (event: RequestEvent) => {
104-
try {
105-
const data = await event.request.formData();
106-
const file = data.get('file');
107-
const emailPath = data.get('path');
109+
'create-email': async (event: RequestEvent) => {
110+
try {
111+
const data = await event.request.formData();
112+
const file = data.get('file');
113+
const emailPath = data.get('path');
108114

109-
if (!file || !emailPath) {
110-
return {
111-
status: 400,
112-
body: { error: 'Missing file or path parameter' }
113-
};
114-
}
115+
if (!file || !emailPath) {
116+
return {
117+
status: 400,
118+
body: { error: 'Missing file or path parameter' }
119+
};
120+
}
115121

116-
const emailComponent = await getEmailComponent(emailPath as string, file as string);
122+
const emailComponent = await getEmailComponent(emailPath as string, file as string);
117123

118-
// Render the component to HTML
119-
const { body } = render(emailComponent);
124+
// Render the component to HTML
125+
const { body } = render(emailComponent);
120126

121-
// Remove all HTML comments from the body before formatting
122-
const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
123-
const formattedBody = await prettier.format(bodyWithoutComments, {
124-
parser: 'html',
125-
plugins: [parserHtml]
126-
});
127+
// Remove all HTML comments from the body before formatting
128+
const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
129+
const formattedBody = await prettier.format(bodyWithoutComments, {
130+
parser: 'html',
131+
plugins: [parserHtml]
132+
});
127133

128-
return {
129-
body: formattedBody
130-
};
131-
} catch (error) {
132-
console.error('Error rendering email:', error);
133-
return {
134-
status: 500,
135-
error: {
136-
message: error instanceof Error ? error.message : 'Failed to render email'
137-
}
138-
};
139-
}
140-
}
134+
return {
135+
body: formattedBody
136+
};
137+
} catch (error) {
138+
console.error('Error rendering email:', error);
139+
return {
140+
status: 500,
141+
error: {
142+
message: error instanceof Error ? error.message : 'Failed to render email'
143+
}
144+
};
145+
}
146+
}
141147
};
142148

143149
export declare const SendEmailFunction: (
144-
{ from, to, subject, html }: { from: string; to: string; subject: string; html: string },
145-
resendApiKey?: string
150+
{ from, to, subject, html }: { from: string; to: string; subject: string; html: string },
151+
resendApiKey?: string
146152
) => Promise<{ success: boolean; error?: any }>;
147153

148154
const defaultSendEmailFunction: typeof SendEmailFunction = async (
149-
{ from, to, subject, html },
150-
resendApiKey
155+
{ from, to, subject, html },
156+
resendApiKey
151157
) => {
152-
// stringify api key to comment out temp
153-
const resend = new Resend(resendApiKey);
154-
const email = { from, to, subject, html };
155-
const resendReq = await resend.emails.send(email);
158+
// stringify api key to comment out temp
159+
const resend = new Resend(resendApiKey);
160+
const email = { from, to, subject, html };
161+
const resendReq = await resend.emails.send(email);
156162

157-
if (resendReq.error) {
158-
return { success: false, error: resendReq.error };
159-
} else {
160-
return { success: true, error: null };
161-
}
163+
if (resendReq.error) {
164+
return { success: false, error: resendReq.error };
165+
} else {
166+
return { success: true, error: null };
167+
}
162168
};
163169

164170
/**
@@ -179,95 +185,95 @@ const defaultSendEmailFunction: typeof SendEmailFunction = async (
179185
* ```
180186
*/
181187
export const sendEmail = ({
182-
customSendEmailFunction,
183-
resendApiKey
188+
customSendEmailFunction,
189+
resendApiKey
184190
}: {
185-
customSendEmailFunction?: typeof SendEmailFunction;
186-
resendApiKey?: string;
191+
customSendEmailFunction?: typeof SendEmailFunction;
192+
resendApiKey?: string;
187193
} = {}) => {
188-
return {
189-
'send-email': async (event: RequestEvent): Promise<{ success: boolean; error: any }> => {
190-
const data = await event.request.formData();
191-
const emailPath = data.get('path');
192-
const file = data.get('file');
194+
return {
195+
'send-email': async (event: RequestEvent): Promise<{ success: boolean; error: any }> => {
196+
const data = await event.request.formData();
197+
const emailPath = data.get('path');
198+
const file = data.get('file');
193199

194-
if (!file || !emailPath) {
195-
return {
196-
success: false,
197-
error: { message: 'Missing file or path parameter' }
198-
};
199-
}
200+
if (!file || !emailPath) {
201+
return {
202+
success: false,
203+
error: { message: 'Missing file or path parameter' }
204+
};
205+
}
200206

201-
const emailComponent = await getEmailComponent(emailPath as string, file as string);
207+
const emailComponent = await getEmailComponent(emailPath as string, file as string);
202208

203-
const email = {
204-
from: 'svelte-email-tailwind <onboarding@resend.dev>',
205-
to: `${data.get('to')}`,
206-
subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
207-
html: (await render(emailComponent)).body
208-
};
209+
const email = {
210+
from: 'svelte-email-tailwind <onboarding@resend.dev>',
211+
to: `${data.get('to')}`,
212+
subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
213+
html: (await render(emailComponent)).body
214+
};
209215

210-
let sent: { success: boolean; error?: any } = { success: false, error: null };
216+
let sent: { success: boolean; error?: any } = { success: false, error: null };
211217

212-
if (!customSendEmailFunction && resendApiKey) {
213-
sent = await defaultSendEmailFunction(email, resendApiKey);
214-
} else if (customSendEmailFunction) {
215-
sent = await customSendEmailFunction(email, resendApiKey);
216-
} else if (!customSendEmailFunction && !resendApiKey) {
217-
const error = {
218-
message:
219-
'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
220-
};
221-
return { success: false, error };
222-
}
218+
if (!customSendEmailFunction && resendApiKey) {
219+
sent = await defaultSendEmailFunction(email, resendApiKey);
220+
} else if (customSendEmailFunction) {
221+
sent = await customSendEmailFunction(email, resendApiKey);
222+
} else if (!customSendEmailFunction && !resendApiKey) {
223+
const error = {
224+
message:
225+
'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
226+
};
227+
return { success: false, error };
228+
}
223229

224-
if (sent && sent.error) {
225-
console.log('Error:', sent.error);
226-
return { success: false, error: sent.error };
227-
} else {
228-
console.log('Email was sent successfully.');
229-
return { success: true, error: null };
230-
}
231-
}
232-
};
230+
if (sent && sent.error) {
231+
console.log('Error:', sent.error);
232+
return { success: false, error: sent.error };
233+
} else {
234+
console.log('Email was sent successfully.');
235+
return { success: true, error: null };
236+
}
237+
}
238+
};
233239
};
234240

235241
// Recursive function to get files
236242
function getFiles(dir: string, files: string[] = []) {
237-
// Get an array of all files and directories in the passed directory using fs.readdirSync
238-
const fileList = fs.readdirSync(dir);
239-
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
240-
for (const file of fileList) {
241-
const name = path.join(dir, file);
242-
// Check if the current file/directory is a directory using fs.statSync
243-
if (fs.statSync(name).isDirectory()) {
244-
// If it is a directory, recursively call the getFiles function with the directory path and the files array
245-
getFiles(name, files);
246-
} else {
247-
// If it is a file, push the full path to the files array
248-
files.push(name);
249-
}
250-
}
251-
return files;
243+
// Get an array of all files and directories in the passed directory using fs.readdirSync
244+
const fileList = fs.readdirSync(dir);
245+
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
246+
for (const file of fileList) {
247+
const name = path.join(dir, file);
248+
// Check if the current file/directory is a directory using fs.statSync
249+
if (fs.statSync(name).isDirectory()) {
250+
// If it is a directory, recursively call the getFiles function with the directory path and the files array
251+
getFiles(name, files);
252+
} else {
253+
// If it is a file, push the full path to the files array
254+
files.push(name);
255+
}
256+
}
257+
return files;
252258
}
253259

254260
/**
255261
* Creates an array of names from the record of svelte email component file paths
256262
*/
257263
function createEmailComponentList(root: string, paths: string[]) {
258-
const emailComponentList: string[] = [];
264+
const emailComponentList: string[] = [];
259265

260-
paths.forEach((filePath) => {
261-
if (filePath.includes(`.svelte`)) {
262-
const fileName = filePath.substring(
263-
filePath.indexOf(root) + root.length + 1,
264-
filePath.indexOf('.svelte')
265-
);
266-
emailComponentList.push(fileName);
267-
}
268-
});
266+
paths.forEach((filePath) => {
267+
if (filePath.includes(`.svelte`)) {
268+
// Compute the path relative to the provided root and normalize separators
269+
// so the list displays consistently across platforms (use forward slashes)
270+
const relative = path.relative(root, filePath).replace(/\\/g, '/');
271+
const fileName = relative.replace(/\.svelte$/i, '');
272+
emailComponentList.push(fileName);
273+
}
274+
});
269275

270-
return emailComponentList;
276+
return emailComponentList;
271277
}
272278

273279
// Export the Preview component

0 commit comments

Comments
 (0)