- The solution must be 100% backward-compatible.
- In the future, developers should be able to write Node programs and libraries without knowledge of the CommonJS module system.
- Module resolution rules should be reasonably compatible with the module resolution rules used by browsers.
- The ability to import a legacy package is important for adoption.
1. There is no change to the behavior of require
. It cannot be used to import ES modules.
This ensures 100% backward-compatibility, while still allowing some freedom of design.
2. Instead of "index.js", the entry point for ES modules is "default.js", or instead of a package.json "main", "default" is used.
A distinct entry point ("default.js") allows us to distinguish when a user is attempting to import from a legacy package or a folder containing CommonJS modules.
3. When import
ing a file path, file extensions are not automatically appended.
The default resolution algorithm used by web browsers will not automatically append file extensions.
4. When import
ing a directory, if a "default.js" file cannot be found, the algorithm will attempt to find an entry point using legacy require
rules, by consulting "package.json" and looking for "index.*" files.
This provides users with the ability to import
from legacy packages.
5. import(modulePath)
asynchronously imports an ES module from CommonJS.
This allows old-style modules to import
from new-style modules.
6. Node will support a --module
flag.
This provides the context that the module being loaded is a module, where in future this could be set by default.
An experimental implementation of this proposal is available at https://github.com/guybedford/node/tree/module-default, supporting NodeJS usage:
node --experimental-modules --module x.js
node --experimental-modules -m x.js
node --experimental-modules -m -e "export var hello = 'world'"
Since there is no change to the behavior of require
, there is no change to the behavior of existing modules and packages.
If a "default.js" file or "default" main does not exist in the package root, then it will be loaded as an old-style module with no further changes. It just works.
Since require
cannot be directly used to import ES modules, we need to provide an old-style "index.js" entry point if we want to allow consumers to require
our package:
src/
[ES modules]
default.js -> src/default.js
index.js
The purpose of the "index.js" file will be to map the ES module into an old-style module and can be as simple as:
// [index.js]
module.exports = import('./src/default.js');
In this usage scenario, a package is authored in ES modules and transpiled to old-style modules using a compiler like Babel. A typical directory layout for such a project is:
lib/
[Transpiled modules]
src/
[ES modules]
index.js -> lib/index.js
Users that require
the package will load the transpiled version of the code. If we want to allow import
ing of this package, we can add a "default.js" file.
lib/
[Transpiled modules]
src/
[ES modules]
index.js -> lib/index.js
default.js -> src/index.js
We might also want our transpiler to rename "default.js" source files to "index.js".
lib/
[Transpiled modules]
src/
[ES modules]
index.js -> lib/index.js
default.js -> src/default.js
In this scenario, a user has a large project and wants to convert old-style modules to new style modules gradually.
Option 1: Using a transpiler
The project uses a transpiler to convert all code to old-style modules. Old-style modules are distributed to consumers. When all modules have been migrated, the transpiler can be removed.
Option 2: Replacing require sites
When converting an old-style module to the ES module syntax, use a script to update all internal modules which reference the converted module. The script would change occurrences of:
var someModule = require('./some-module');
to:
var someModule = (await import('./some-module.js')).default;
A common practice with old-style packages is to allow the user to require
individual modules within the package source:
// Loads node_modules/foo/bar.js
var deepModule = require('foo/bar');
If the package author wants to support both require
ing and import
ing into a nested module, they might do so by creating a folder for each "deep link", which contains both an old-style and new-style entry point:
bar/
index.js (Entry point for require)
default.js (Entry point for import)
- "default.html" is frequently used as a folder entry point for web servers.
- The word "default" has a special, and similar meaning in ES modules.
- Despite "default" being a common English word, "default.js" is not widely used as a file name.
In a search of all the filenames in the @latest NPM packages as of 2016-01-28, "default.js" was only found 23 times in a package root. Of these packages, 8 are using "default.js" as an ES module entry point already (they are published by @zenparsing, so no surprises there). The remaining 15 packages would need to be updated in order to allow import
ing them from other ES modules.
As a filename, "default.js" was found 1968 times.
If when testing this proposal it turns out that using the package.json "module" property instead of "default" works in a large percentage of cases of existing usage, then it could be considered to use
"default.js"
and"module"
in the package.json file. But this would have to be based on ensuring the tests have been run against common packages to verify that such compatibility will be supported.
When a user executes
$ node my-module.js
from the command line, there is absolutely no way for Node to tell whether "my-module.js" is a legacy CJS module or an ES module. Due to the need of this knowledge for various interactive scenarios such as the entry file being provided over STDIN, node will support a --module
flag.
$ node --module my-module.js
Loads X from a module at path Y. T is either "require" or "import".
- If X is a core module, then
- return the core module
- STOP
- If X begins with './' or '/' or '../'
- LOAD_AS_FILE(Y + X, T)
- LOAD_AS_DIRECTORY(Y + X, T)
- LOAD_NODE_MODULES(X, dirname(Y), T)
- THROW "not found"
- If T is "import",
- If X is a file, then
- If extname(X) is ".js", load X as ES module text. STOP
- If extname(X) is ".json", parse X to a JavaScript Object. STOP
- If extname(X) is ".node", load X as binary addon. STOP
- THROW "not found"
- If X is a file, then
- Else,
- Assert: T is "require"
- If X is a file, load X as CJS module text. STOP
- If X.js is a file, load X.js as CJS module text. STOP
- If X.json is a file, parse X.json to a JavaScript Object. STOP
- If X.node is a file, load X.node as binary addon. STOP
- If T is "import",
- If X/default.js is a file, load X/default.js as ES module text. STOP
- If X/package.json is a file,
- Parse X/package.json, and look for "default" field.
- load X/(json module field) as ES module text. STOP
- NOTE: If neither of the above are a file, then fallback to legacy behavior
- If X/package.json is a file,
- Parse X/package.json, and look for "main" field.
- let M = X + (json main field)
- LOAD_AS_FILE(M, "require")
- If X/index.js is a file, load X/index.js as JavaScript text. STOP
- If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
- If X/index.node is a file, load X/index.node as binary addon. STOP
- let DIRS=NODE_MODULES_PATHS(START)
- for each DIR in DIRS:
- LOAD_AS_FILE(DIR/X, T)
- LOAD_AS_DIRECTORY(DIR/X, T)