Node 2540 solves the import/require mess by introducing a unified module resolution mode that lets you use both import and require() interchangeably in the same file, and it adds several long-requested features like built-in TypeScript type stripping, a stable --watch mode, and a built-in test runner with snapshot support. This article explains what changed, why it matters, and how to use the new capabilities without breaking existing projects.
Background: The Import/Require Conflict
For years, Node.js developers have had to choose between CommonJS (require()) and ES Modules (import). The two systems were incompatible: you couldn’t mix them in the same file, and importing a CommonJS module from an ES module often required workarounds like createRequire() or dynamic import(). This caused confusion, especially for beginners and for packages that wanted to support both ecosystems.
Node 2540 (a hypothetical future version, but consistent with Node’s actual roadmap) finally unifies the two. The new resolution algorithm lets you use import to load CommonJS modules and require() to load ES modules, with a few documented edge cases. The goal is to make the module system “just work” regardless of which syntax you prefer.
How the Unified Module Resolution Works
The core change is that Node now treats .js files as “ambiguous” unless you specify a type field in your package.json. When a file is ambiguous, Node looks at how it’s being loaded: if it’s loaded with require(), it’s treated as CommonJS; if loaded with import, it’s treated as an ES module. This is similar to how browsers treat <script type="module"> vs. classic scripts.
For files that explicitly declare "type": "module" or "type": "commonjs", the behavior is unchanged. But for ambiguous files, you can now do:
// file.cjs (or ambiguous .js loaded with require)
const { readFile } = require('fs/promises');
// file.mjs (or ambiguous .js loaded with import)
import { readFile } from 'fs/promises';
You can even mix both in the same file if you use dynamic import() inside a CommonJS file, or createRequire() inside an ES module — but the new feature makes this unnecessary in most cases.
New Features in Node 2540
Besides the module resolution overhaul, Node 2540 ships with several features that have been in development for a long time:
- Built-in TypeScript type stripping: You can run
.tsfiles directly without a separate compiler. Node strips the types (no type checking) and runs the JavaScript. This works for bothimportandrequire()syntax. - Stable
--watchmode: The--watchflag, previously experimental, is now stable. It restarts your process when any imported file changes, and it works withnode --watch server.js. - Built-in test runner with snapshot testing: The
node:testmodule now supportst.assert.snapshot()to create and compare snapshots, similar to Jest or Vitest. - Improved error messages for module resolution: When a module can’t be found, Node now suggests possible fixes, like installing the package or checking the file extension.
- Support for
import.meta.dirnameandimport.meta.filename: These provide the directory and filename of the current module, without needingfileURLToPath(import.meta.url).
How to Choose Between import and require
With the unified resolution, the choice is mostly stylistic. Here are some guidelines:
- If you’re writing a new library, use ES modules (
import) because they are the future and work in browsers too. - If you’re maintaining a legacy CommonJS project, you can keep using
require()and still import ES modules when needed. - If you’re writing a script that runs in both Node and the browser, use
import. - If you need dynamic loading based on a variable, use
require()(it’s synchronous) orimport()(it’s asynchronous).
In practice, you can now write a file with import and then use require() in the same file (via createRequire) if you really need it, but the new resolution means you rarely will.
Common Problems and How to Avoid Them
Even with the unified resolution, there are pitfalls:
- File extensions matter: In ES modules, you must include the full extension (e.g.,
import './file.js'). In CommonJS, you can omit it. The new resolution doesn’t change this. - Circular dependencies: Mixing
importandrequirecan cause circular dependency issues. Stick to one style per file to minimize risk. - TypeScript type stripping is not type checking: You still need
tsc --noEmitfor type safety. The stripping only removes types. - Package
exportsfield: If you’re publishing a package, make sure yourexportsfield defines bothimportandrequireconditions so consumers using either syntax can load it.
Comparisons with Other Runtimes
Node 2540’s unified module resolution brings it closer to Deno and Bun, which already support both syntaxes. Deno uses URLs and has a different module resolution, while Bun uses a package manager that installs dependencies automatically. Node’s approach is more conservative, focusing on backward compatibility with the npm ecosystem.
For developers, the practical difference is that Node still requires a package.json for most projects, while Deno can run a single file without any configuration. But Node’s advantage is its massive ecosystem and mature tooling.
Costs and Migration
Upgrading to Node 2540 is free, but migrating a large codebase may require effort:
- If you have a
"type": "commonjs"package, you can keep it and useimport()for ES modules. - If you have a
"type": "module"package, you can userequire()for CommonJS modules, but you may need to update yourexportsfield. - Running
.tsfiles directly is convenient, but you should still have a build step for production if you need type checking or target older Node versions.
There’s no cost to using the new features; they’re part of the runtime. The main cost is learning the new behavior and updating any code that relied on the old restrictions.
Safety and Stability
The unified module resolution is a breaking change for some edge cases. For example, a file that was previously treated as CommonJS because it had no type field might now be treated as an ES module if it’s loaded with import. To avoid surprises, Node 2540 logs a warning when it detects ambiguous files that are loaded with both syntaxes. The recommendation is to explicitly set "type" in your package.json for any package you publish.
The --watch mode is stable, but it’s not recommended for production use because it restarts the process on every file change, which can cause downtime. Use a process manager like PM2 or systemd for production.
The built-in test runner is still relatively new, but it’s actively maintained. Snapshot testing is useful, but be careful when updating snapshots: always review the diff before accepting changes.
Frequently Asked Questions
Can I use import and require in the same file now?
Yes, but with a caveat. You can use import at the top level of a CommonJS file, and you can use require() inside an ES module if you create a createRequire instance. The new resolution makes it easier to load modules of the other type, but mixing syntaxes in the same file is still not recommended because it can lead to confusion and circular dependency issues.
Will my existing CommonJS code break after upgrading?
Mostly no. If your package has "type": "commonjs" or uses .cjs extensions, nothing changes. If you have ambiguous .js files without a type field, they will now be treated as CommonJS when loaded with require() and as ES modules when loaded with import. This could break code that relied on the old behavior, but Node will warn you.
Does Node 2540 support TypeScript out of the box?
Yes, for running .ts files directly. Node strips the types and runs the JavaScript. It does not perform type checking, so you should still run tsc --noEmit in your build process. This feature is experimental but works for most code.
How do I use the new snapshot testing?
In your test file, import node:test and use t.assert.snapshot(value). The first time you run it, it creates a snapshot file. On subsequent runs, it compares the value to the snapshot. To update snapshots, run node --test --update-snapshots.
Is –watch mode safe for production?
No. --watch restarts your process whenever a file changes, which is useful for development but not for production. For production, use a process manager that handles restarts gracefully and monitors health.
What is the difference between import.meta.dirname and __dirname?
__dirname is available in CommonJS and gives the directory of the current module. import.meta.dirname is the equivalent in ES modules. In Node 2540, you can use import.meta.dirname in both ES modules and CommonJS (if you use import.meta), but __dirname is still only in CommonJS.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Delete Picture Kik? How to Remove Photos
- What Happens to X Replies, DMs, and Followers After Deletion?
- Data Breaches That Shook the World
- Disable Google Assistant: Turn It Off on Android & Home
- Resin vs FDM Printers for Miniatures: Detail, Durability, Cost
- Are Networked Implants Safe? What If One Fails?