keepcalculator

06 Numbers and Powers

Node 2540: Import Require Fix and New Features

Card No. 06·27253 · Updated · Standard formula, shown below

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 .ts files directly without a separate compiler. Node strips the types (no type checking) and runs the JavaScript. This works for both import and require() syntax.
  • Stable --watch mode: The --watch flag, previously experimental, is now stable. It restarts your process when any imported file changes, and it works with node --watch server.js.
  • Built-in test runner with snapshot testing: The node:test module now supports t.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.dirname and import.meta.filename: These provide the directory and filename of the current module, without needing fileURLToPath(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) or import() (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 import and require can cause circular dependency issues. Stick to one style per file to minimize risk.
  • TypeScript type stripping is not type checking: You still need tsc --noEmit for type safety. The stripping only removes types.
  • Package exports field: If you’re publishing a package, make sure your exports field defines both import and require conditions 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 use import() for ES modules.
  • If you have a "type": "module" package, you can use require() for CommonJS modules, but you may need to update your exports field.
  • Running .ts files 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

The whole Numbers and Powers drawer →