Next.js: ignore test files in route folders (pages)

When working with Next.js, I prefer to keep my test files right next to the code they’re testing. For example:

This makes it easy to find tests, spot missing coverage, and avoids duplicating folder structures in a separate __tests__ directory.

But there’s a catch: Next.js routes (pages) from every file in the pages directory.

Next.js: Batteries Footguns Included #

Next.js treats every file in the pages directory as a route, so welcome.test.tsx becomes a /welcome.test route.

src/components/Foo.tsx
src/components/Foo.test.tsx // fine, not a route
src/pages/welcome.tsx
src/pages/welcome.test.tsx  // Next.js tries to make a /welcome.test route

This ends up bundling test code into your client bundle! Assuming this doesn’t fail outright during a build, you can wind up exposing sensitive code this way.

The official workaround is to add a .page segment to every route file (like, /welcome.page.tsx), then configure Next.js to only treat those files as pages. This works, but it relies on developers knowing about and remembering this convention every time they create a new file, and it clutters up file names. It’s easy to forget to include the name and then be left scratching your head about why the route 404s in the browser.

This has frequently annoyed me. Frameworks ought to make doing things the right way easy and as invisible as possible. Shipping test code in my production bundle is neither, and the solution of slapping a suffix on every one of my route files is noisy.

I wanted a better solution, and I found one.

A real solution to exclude test files in Next.js routes #

When next build is run, Next.js uses the value of the pageExtensions directive (from next.config.js) in a regular expression without any escaping. This means regex patterns can be included! And since Node now supports negative lookbehind assertions, it’s possible to write patterns that negate specific character sequences without matching segments or needing to negate the code around the pattern. That means something like this is now possible:

const pattern = /(?<!\.test\.)ts/
pattern.test("foo.ts")        // true
pattern.test("foo.test.ts")   // false

The only thing left to put this into practice is to convert the regex to a string (escaping backslashes as needed) and updating next.config.js like so:

next.config.js #

/**
 * Exclude `.test.{ext}` files from being treated as pages.
 *
 * @param {string[]} pageExtensions
 * @returns {string[]}
 */
const excludeTestFiles = (pageExtensions) =>
  pageExtensions.map((ext) => `(?<!\\.test\\.)${ext}`)

/** @type {import('next').NextConfig} */
export default {
  pageExtensions: excludeTestFiles(["ts", "tsx", "js", "jsx"]),
  // …the rest of your config
}

What does this do? #

This code configures Next.js to only treat files as routes if their extension isn’t immediately preceded by ‎.test.. This means ‎welcome.tsx is a route, but ‎welcome.test.tsx is ignored—no accidental test routes, no extra naming conventions.

With this approach, you can keep your tests co-located with your code and maintain a clean project structure without ever thinking about it! 🎉

 
2
Kudos
 
2
Kudos

Now read this

Render templates from anywhere in Ruby on Rails

Rails 5 recently shipped, and among many other new features is a new renderer that makes it easy to render fully composed views outside of your controllers. This comes in handy if you want to attach, say, an HTML receipt to an order... Continue →