|
| 1 | +# A Modest Proposal for ES6 Modules in Node.js |
| 2 | + |
| 3 | +## Guiding Principles |
| 4 | + |
| 5 | +- The solution must be 100% backward-compatible. |
| 6 | +- In the far future, developers should be able to write Node programs and libraries without knowledge of the CommonJS module system. |
| 7 | +- Module resolution rules should be reasonably compatible with the module resolution rules used by browsers. |
| 8 | +- The ability to import a legacy package is important for adoption. |
| 9 | + |
| 10 | +## Design Summary |
| 11 | + |
| 12 | +**1. There is no change to the behavior of `require`. It cannot be used to import ES6 modules.** |
| 13 | + |
| 14 | +This ensures 100% backward-compatibility, while still allowing some freedom of design. |
| 15 | + |
| 16 | +**2. The only folder entry point for ES6 modules is "default.js". "package.json" files are not used for resolving ES6 module paths.** |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +**3. When `import`ing a file path, file extensions are not automatically appended.** |
| 21 | + |
| 22 | +The default resolution algorithm used by web browsers will not automatically append file extensions. |
| 23 | + |
| 24 | +**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.** |
| 25 | + |
| 26 | +This provides users with the ability to `import` from legacy packages. |
| 27 | + |
| 28 | +**5. `require.import(modulePath)` synchronously imports an ES6 module.** |
| 29 | + |
| 30 | +This allows old-style modules to `import` from new-style modules. |
| 31 | + |
| 32 | +## Use Cases |
| 33 | + |
| 34 | +### Existing modules |
| 35 | + |
| 36 | +Since there is no change to the behavior of `require`, there is no change to the behavior of existing modules and packages. |
| 37 | + |
| 38 | +### Supporting `import` for old-style packages |
| 39 | + |
| 40 | +If a "default.js" file does not exist in the package root, then it will be loaded as an old-style module with no further changes. It just works. |
| 41 | + |
| 42 | +### Supporting `require` for ES6 packages |
| 43 | + |
| 44 | +Since `require` cannot be directly used to import ES6 modules, we need to provide an old-style "index.js" entry point if we want to allow consumers to `require` our package: |
| 45 | + |
| 46 | +``` |
| 47 | +src/ |
| 48 | + [ES6 modules] |
| 49 | +default.js -> src/default.js |
| 50 | +index.js |
| 51 | +``` |
| 52 | + |
| 53 | +The purpose of the "index.js" file will be to map the ES6 module into an old-style module and can be as simple as: |
| 54 | + |
| 55 | +```js |
| 56 | +// [index.js] |
| 57 | +module.exports = require.import('./src/default.js'); |
| 58 | +``` |
| 59 | + |
| 60 | +### Distributing both transpiled and native ES6 modules |
| 61 | + |
| 62 | +In this usage scenario, a package is authored in ES6 modules and transpiled to old-style modules using a compiler like Babel. A typical directory layout for such a project is: |
| 63 | + |
| 64 | +``` |
| 65 | +lib/ |
| 66 | + [Transpiled modules] |
| 67 | +src/ |
| 68 | + [ES6 modules] |
| 69 | +index.js -> lib/index.js |
| 70 | +``` |
| 71 | + |
| 72 | +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. |
| 73 | + |
| 74 | +``` |
| 75 | +lib/ |
| 76 | + [Transpiled modules] |
| 77 | +src/ |
| 78 | + [ES6 modules] |
| 79 | +index.js -> lib/index.js |
| 80 | +default.js -> src/index.js |
| 81 | +``` |
| 82 | + |
| 83 | +We might also want our transpiler to rename "default.js" source files to "index.js". |
| 84 | + |
| 85 | +``` |
| 86 | +lib/ |
| 87 | + [Transpiled modules] |
| 88 | +src/ |
| 89 | + [ES6 modules] |
| 90 | +index.js -> lib/index.js |
| 91 | +default.js -> src/default.js |
| 92 | +``` |
| 93 | + |
| 94 | +### Gradually migrating a project to ES modules |
| 95 | + |
| 96 | +In this scenario, a user has a large project and wants to convert old-style modules to new style modules gradually. |
| 97 | + |
| 98 | +**Option 1: Using a transpiler** |
| 99 | + |
| 100 | +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. |
| 101 | + |
| 102 | +**Option 2: Replacing require sites** |
| 103 | + |
| 104 | +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: |
| 105 | + |
| 106 | +```js |
| 107 | +var someModule = require('./some-module'); |
| 108 | +``` |
| 109 | + |
| 110 | +to: |
| 111 | + |
| 112 | +```js |
| 113 | +var someModule = require.import('./some-module.js').default; |
| 114 | +``` |
| 115 | + |
| 116 | +### Deep-linking into a package |
| 117 | + |
| 118 | +A common practice with old-style packages is to allow the user to `require` individual modules within the package source: |
| 119 | + |
| 120 | +```js |
| 121 | +// Loads node_modules/foo/bar.js |
| 122 | +var deepModule = require('foo/bar'); |
| 123 | +``` |
| 124 | + |
| 125 | +If the package author wants to support both `require`ing and `import`ing into a nested module, they can do so by creating a folder for each "deep link", which contains both an old-style and new-style entry point: |
| 126 | + |
| 127 | +``` |
| 128 | +bar/ |
| 129 | + index.js (Entry point for require) |
| 130 | + default.js (Entry point for import) |
| 131 | +``` |
| 132 | + |
| 133 | +## Why "default.js"? |
| 134 | + |
| 135 | +- "default.html" is frequently used as a folder entry point for web servers. |
| 136 | +- The word "default" has a special, and similar meaning in ES6 modules. |
| 137 | +- Despite "default" being a common English word, "default.js" is not widely used as a file name. |
| 138 | + |
| 139 | +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. |
| 140 | + |
| 141 | +## Running Modules from the Command Line |
| 142 | + |
| 143 | +When a user executes |
| 144 | + |
| 145 | +```sh |
| 146 | +$ node my-module.js |
| 147 | +``` |
| 148 | + |
| 149 | +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. |
| 150 | + |
| 151 | +## Lookup Algorithm Psuedo-Code |
| 152 | + |
| 153 | +### LOAD_MODULE(X, Y, T) |
| 154 | + |
| 155 | +Loads _X_ from a module at path _Y_. _T_ is either "require" or "import". |
| 156 | + |
| 157 | +1. If X is a core module, then |
| 158 | + 1. return the core module |
| 159 | + 1. STOP |
| 160 | +1. If X begins with './' or '/' or '../' |
| 161 | + 1. LOAD_AS_FILE(Y + X, T) |
| 162 | + 1. LOAD_AS_DIRECTORY(Y + X, T) |
| 163 | +1. LOAD_NODE_MODULES(X, dirname(Y), T) |
| 164 | +1. THROW "not found" |
| 165 | + |
| 166 | +### LOAD_AS_FILE(X, T) |
| 167 | + |
| 168 | +1. If T is "import", |
| 169 | + 1. If X is a file, then |
| 170 | + 1. If extname(X) is ".js", load X as ES6 module text. STOP |
| 171 | + 1. If extname(X) is ".json", parse X to a JavaScript Object. STOP |
| 172 | + 1. If extname(X) is ".node", load X as binary addon. STOP |
| 173 | + 1. THROW "not found" |
| 174 | +1. Else, |
| 175 | + 1. Assert: T is "require" |
| 176 | + 1. If X is a file, load X as CJS module text. STOP |
| 177 | + 1. If X.js is a file, load X.js as CJS module text. STOP |
| 178 | + 1. If X.json is a file, parse X.json to a JavaScript Object. STOP |
| 179 | + 1. If X.node is a file, load X.node as binary addon. STOP |
| 180 | + |
| 181 | +### LOAD_AS_DIRECTORY(X, T) |
| 182 | + |
| 183 | +1. If T is "import", |
| 184 | + 1. If X/default.js is a file, load X/default.js as ES6 module text. STOP |
| 185 | + 1. NOTE: If X/default.js is not a file, then fallback to legacy behavior |
| 186 | +1. If X/package.json is a file, |
| 187 | + 1. Parse X/package.json, and look for "main" field. |
| 188 | + 1. let M = X + (json main field) |
| 189 | + 1. LOAD_AS_FILE(M, "require") |
| 190 | +1. If X/index.js is a file, load X/index.js as JavaScript text. STOP |
| 191 | +1. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP |
| 192 | +1. If X/index.node is a file, load X/index.node as binary addon. STOP |
| 193 | + |
| 194 | +### LOAD_NODE_MODULES(X, START, T) |
| 195 | + |
| 196 | +1. let DIRS=NODE_MODULES_PATHS(START) |
| 197 | +2. for each DIR in DIRS: |
| 198 | + 1. LOAD_AS_FILE(DIR/X, T) |
| 199 | + 1. LOAD_AS_DIRECTORY(DIR/X, T) |
0 commit comments