Skip to content

Instantly share code, notes, and snippets.

@zenparsing
Last active November 9, 2021 15:50
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zenparsing/1a84f0eb83d2db4a4736 to your computer and use it in GitHub Desktop.
Save zenparsing/1a84f0eb83d2db4a4736 to your computer and use it in GitHub Desktop.
Node Module Lookup Rules

ES6 Module Loading in Node.js

Guiding Principles

  • The solution must be 100% backward-compatible.
  • In the far 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.

Design Summary

There is no change to the behavior of require. It cannot be used to import ES6 modules.

Motivation: This ensures 100% backward-compatibility, while still allowing some freedom of design.

The only folder entry point for ES6 modules is "default.js". "package.json" files are not used for resolving ES6 module paths.

Motivation: A distinct entry point file name ("default.js") allows us to detect when a user is attempting to import from a legacy package or a folder containing legacy modules.

When importing a file path, file extensions are not automatically appended.

Motivation: The default resolution algorithm used by web browsers will not automatically append file extensions.

When importing 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.

Motivation: This provides users with the ability to import from legacy packages.

Why "default.js"?

  • "default.html" is frequently used as a folder entry point for web servers.
  • The word "default" has a special, and similar meaning in ES6 modules.
  • Despite "default" being a common English word, "default.js" is not widely used as a file name.

In a random sampling of 25,000 NPM packages (10% of the total number of packages), "default.js" was only found one time in a package root. This particular "default.js" file was already an ES6 module. As a filename, "default.js" was found only 174 times. By contrast, "index.js" was found 22,607 times, and in the package root 10,282 times.

Lookup Algorithm Psuedo-Code

LOAD_MODULE(X, Y, T)

Loads X from a module at path Y. T is either "require" or "import".

  1. If X is a core module, then
    1. return the core module
    2. STOP
  2. If X begins with './' or '/' or '../'
    1. LOAD_AS_FILE(Y + X, T)
    2. LOAD_AS_DIRECTORY(Y + X, T)
  3. LOAD_NODE_MODULES(X, dirname(Y), T)
  4. THROW "not found"

LOAD_AS_FILE(X, T)

  1. If T is "import",
    1. If X is a file, then
      1. If extname(X) is ".js", load X as ES6 module text. STOP
      2. If extname(X) is ".json", parse X to a JavaScript Object. STOP
      3. If extname(X) is ".node", load X as binary addon. STOP
      4. THROW "not found"
  2. Else,
    1. Assert: T is "require"
    2. If X is a file, load X as CJS module text. STOP
    3. If X.js is a file, load X.js as CJS module text. STOP
    4. If X.json is a file, parse X.json to a JavaScript Object. STOP
    5. If X.node is a file, load X.node as binary addon. STOP

LOAD_AS_DIRECTORY(X, T)

  1. If T is "import",
    1. If X/default.js is a file, load X/default.js as ES6 module text. STOP
    2. NOTE: If X/default.js is not a file, then fallback to legacy behavior
  2. If X/package.json is a file,
    1. Parse X/package.json, and look for "main" field.
    2. let M = X + (json main field)
    3. LOAD_AS_FILE(M, "require")
  3. If X/index.js is a file, load X/index.js as JavaScript text. STOP
  4. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
  5. If X/index.node is a file, load X/index.node as binary addon. STOP

LOAD_NODE_MODULES(X, START, T)

  1. let DIRS=NODE_MODULES_PATHS(START)
  2. for each DIR in DIRS:
    1. LOAD_AS_FILE(DIR/X, T)
    2. LOAD_AS_DIRECTORY(DIR/X, T)

Running Modules from the Command Line

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 ES6 module. In the interest of backward compatibility, Node should probably attempt to load the file as a CJS module, and fallback to ES6 if there is a syntax error indicating the presence of import declarations. As people move away from CJS modules in general, future Node versions can assume that the file is an ES6 module.

@bmeck
Copy link

bmeck commented Feb 11, 2016

Also, would like to note that package.json main is optional: see https://registry.npmjs.com/npm-index-test/1.0.0 & https://registry.npmjs.com/npm-index-test/2.0.0 . In light of this, and the overwhelming number of existing index.js and heavily used index.html in web servers I don't think we need to change from index to default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment