File path not working on deployment to Vercel (Solved)

So I wrote out this long post and ended up solving the issue myself. I figured I would post it in case it helps anyone.

I have a utility function that runs on the server to grab an email template when invoked. I’m creating the path as described by the documentation

const publicFolderPath = path.join(process.cwd(), 'src', 'utils', emailTemplates/${template}.html);

This works fine on my local machine both in dev and running build / start. When I deploy to vercel I get the error:

ENOENT: no such file or directory, open β€˜/var/task/src/utils/emailTemplates/inquiry-information.html’

my current directory structure looks like this:

PROJECT/
β”œβ”€β”€ .next/
β”œβ”€β”€ node_modules/
β”œβ”€β”€ public/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ app/
β”‚ β”‚ β”œβ”€β”€ about/
β”‚ β”‚ β”œβ”€β”€ contact/
β”‚ β”‚ β”‚ β”œβ”€β”€ contact.module.css
β”‚ β”‚ β”‚ └── page.tsx
β”‚ β”‚ β”œβ”€β”€ customers/
β”‚ β”‚ β”œβ”€β”€ privacy-policy/
β”‚ β”‚ β”œβ”€β”€ terms-of-service/
β”‚ β”‚ β”œβ”€β”€ actions.tsx
β”‚ β”‚ β”œβ”€β”€ apple-icon.png
β”‚ β”‚ β”œβ”€β”€ favicon.ico
β”‚ β”‚ β”œβ”€β”€ globals.css
β”‚ β”‚ β”œβ”€β”€ icon.png
β”‚ β”‚ β”œβ”€β”€ icon2.png
β”‚ β”‚ β”œβ”€β”€ icon3.png
β”‚ β”‚ β”œβ”€β”€ layout.tsx
β”‚ β”‚ β”œβ”€β”€ manifest.ts
β”‚ β”‚ β”œβ”€β”€ page.module.css
β”‚ β”‚ └── page.tsx
β”‚ β”œβ”€β”€ components/
β”‚ β”œβ”€β”€ data/
β”‚ β”œβ”€β”€ types/
β”‚ └── utils/
β”‚ └── emailTemplates/
β”‚ β”œβ”€β”€ inquiry-information.html
β”‚ └── inquiry.html
β”‚ β”œβ”€β”€ createEmail.tsx
β”‚ β”œβ”€β”€ emailTransporter.tsx
β”‚ └── toast.ts
β”œβ”€β”€ .env.local
β”œβ”€β”€ .env.production
β”œβ”€β”€ .eslintrc.json
β”œβ”€β”€ .gitignore
β”œβ”€β”€ next-env.d.ts
β”œβ”€β”€ next.config.mjs
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ README.md
└── tsconfig.json

I’ve also tried putting the templates in the public folder and I’ve had the same issues. Here’s my createEmail file as well:

// node utils
import { promises as fs } from 'fs'; // Use fs to read the file asynchronously
import path from 'path';

// nodemailer
import transporter from './emailTransporter';


// types
import { EmailData } from '@/types/email';



const sendEmail = async (to: string, subject: string, template: string, data: EmailData) => {

    try {


        const publicFolderPath = path.join(process.cwd(), 'src', 'utils', `emailTemplates/${template}.html`);

       
        console.log('public path', publicFolderPath);
        console.log('dirname', __dirname);
        console.log('next path', process.cwd());

        // Read the HTML file
        let htmlTemplate = await fs.readFile(publicFolderPath, 'utf-8');
      

        // loop through the data object and replace what exists
        for (const [key, value] of Object.entries(data)) {
            htmlTemplate = htmlTemplate.replaceAll(`{{${key}}}`, String(value));
        }


        const mailOptions = {
            from: 'privateInfo',
            to: to,
            subject: subject,
            html: htmlTemplate,
        };


        const email = await transporter.sendMail(mailOptions);
        console.log('Email information:', email.messageId);

    } catch (error) {

        if (error instanceof Error) {
            console.error('Failed to send email:', error.message);

        } else {
            console.error('An unknown error occurred while sending email');
        }

        // throw this error to stop the second email from attempting to send 
        throw new Error('<Error occurred in createEmail function>');
    }

};


export default sendEmail;

The fix ended up being needing to add this to the next config file:

experimental: {
        outputFileTracingIncludes: {
            '/**': ['./src/utils/emailTemplates/**/*'],
        },
    },

I suspect it has to do with the build treeshaking the imports and not including files that aren’t explicitly included or linked to in something like an img tag. However that wouldn’t explain how the build works locally. If anyone knows exactly why this fix works and if there’s a better way, or if there’s a way to optimize this option please let me know.

2 Likes

:100: You’re awesome for sharing this solution, @operations-greypulse!

I moved this to Community tag to make it easier to find for others :smile:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.