Reading files on Vercel during Next.js ISR
React Server Components are a great addition to Next.js, allowing us to use
server-side APIs in our components, like accessing the file system with fs.readFile
.
Relative file paths in ESM
In ESM, import.meta.url
points to the current file “URL”. For Node.js programs, it looks like this:
file:///absolute/path/to/file.ts
In CommonJS, we used to have __filename
and __dirname
. As an aside,
we can trivially rebuild them in ESM using a little path manipulation:
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
I’m using this extensively in this website to resolve relative paths:
export function resolve(importMetaUrl: string, ...paths: string[]) {
const dirname = path.dirname(fileURLToPath(importMetaUrl))
return path.resolve(dirname, ...paths)
}
// Example usage:
resolve(import.meta.url, './relative/path.txt')
The issue with Vercel and Next.js ISR
This pattern works fine when developping locally and when building the Next.js application on Vercel, but it fails in an Incremental Static Regeneration (ISR) context:
[Error: ENOENT: no such file or directory, open '/vercel/path0/path/to/file.txt'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/vercel/path0/path/to/file.txt'
}
[Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.] {
digest: '2649891783'
}
That’s because ISR runs in serverless functions, which are bundled, and the Next.js file tracer hasn’t been able to pick the file we needed, as it relies on static tracing.
The fix
To tell the file tracer to pick up our file, we have to use process.cwd()
.
Our resolve
function can then be updated like this:
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// src/lib
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const nextJsRootDir = path.resolve(__dirname, '../../')
export function resolve(importMetaUrl: string, ...paths: string[]) {
const dirname = path.dirname(fileURLToPath(importMetaUrl))
const absPath = path.resolve(dirname, ...paths)
// Required for ISR serverless functions to pick up the file path
// as a dependency to bundle.
return path.resolve(process.cwd(), absPath.replace(nextJsRootDir, '.'))
}
And now Vercel doesn’t complain about missing files on ISR updates. 🎉
Freelance developer & founder