├── .gitignore └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | _* 3 | node_modules 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Modest Proposal for ES Modules in Node.js 2 | 3 | ## Guiding Principles 4 | 5 | - The solution must be 100% backward-compatible. 6 | - In the 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 ES modules.** 13 | 14 | This ensures 100% backward-compatibility, while still allowing some freedom of design. 15 | 16 | **2. Instead of "index.js", the entry point for ES modules is "default.js", or instead of a package.json "main", "default" is used.** 17 | 18 | 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. 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. `import(modulePath)` asynchronously imports an ES module from CommonJS.** 29 | 30 | This allows old-style modules to `import` from new-style modules. 31 | 32 | **6. Node will support a `--module` flag.** 33 | 34 | This provides the context that the module being loaded is a module, where in future this could be set by default. 35 | 36 | ## Implementation 37 | 38 | An experimental implementation of this proposal is available at https://github.com/guybedford/node/tree/module-default, supporting NodeJS usage: 39 | 40 | ``` 41 | node --experimental-modules --module x.js 42 | node --experimental-modules -m x.js 43 | node --experimental-modules -m -e "export var hello = 'world'" 44 | ``` 45 | 46 | ## Use Cases 47 | 48 | ### Existing modules 49 | 50 | Since there is no change to the behavior of `require`, there is no change to the behavior of existing modules and packages. 51 | 52 | ### Supporting `import` for old-style packages 53 | 54 | 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. 55 | 56 | ### Supporting `require` for ES Module packages 57 | 58 | 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: 59 | 60 | ``` 61 | src/ 62 | [ES modules] 63 | default.js -> src/default.js 64 | index.js 65 | ``` 66 | 67 | 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: 68 | 69 | ```js 70 | // [index.js] 71 | module.exports = import('./src/default.js'); 72 | ``` 73 | 74 | ### Distributing both transpiled and native ES modules 75 | 76 | 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: 77 | 78 | ``` 79 | lib/ 80 | [Transpiled modules] 81 | src/ 82 | [ES modules] 83 | index.js -> lib/index.js 84 | ``` 85 | 86 | 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. 87 | 88 | ``` 89 | lib/ 90 | [Transpiled modules] 91 | src/ 92 | [ES modules] 93 | index.js -> lib/index.js 94 | default.js -> src/index.js 95 | ``` 96 | 97 | We might also want our transpiler to rename "default.js" source files to "index.js". 98 | 99 | ``` 100 | lib/ 101 | [Transpiled modules] 102 | src/ 103 | [ES modules] 104 | index.js -> lib/index.js 105 | default.js -> src/default.js 106 | ``` 107 | 108 | ### Gradually migrating a project to ES modules 109 | 110 | In this scenario, a user has a large project and wants to convert old-style modules to new style modules gradually. 111 | 112 | **Option 1: Using a transpiler** 113 | 114 | 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. 115 | 116 | **Option 2: Replacing require sites** 117 | 118 | 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: 119 | 120 | ```js 121 | var someModule = require('./some-module'); 122 | ``` 123 | 124 | to: 125 | 126 | ```js 127 | var someModule = (await import('./some-module.js')).default; 128 | ``` 129 | 130 | ### Deep-linking into a package 131 | 132 | A common practice with old-style packages is to allow the user to `require` individual modules within the package source: 133 | 134 | ```js 135 | // Loads node_modules/foo/bar.js 136 | var deepModule = require('foo/bar'); 137 | ``` 138 | 139 | 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: 140 | 141 | ``` 142 | bar/ 143 | index.js (Entry point for require) 144 | default.js (Entry point for import) 145 | ``` 146 | 147 | ## Why "default"? 148 | 149 | - "default.html" is frequently used as a folder entry point for web servers. 150 | - The word "default" has a special, and similar meaning in ES modules. 151 | - Despite "default" being a common English word, "default.js" is not widely used as a file name. 152 | 153 | In a [search of all the filenames in the @latest NPM packages as of 2016-01-28](https://gist.github.com/bmeck/9b234011938cd9c1f552d41db97ad005), "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. 154 | 155 | As a filename, "default.js" was found 1968 times. 156 | 157 | > 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 158 | 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 159 | on ensuring the tests have been run against common packages to verify that such compatibility will be supported. 160 | 161 | ## Running Modules from the Command Line 162 | 163 | When a user executes 164 | 165 | ```sh 166 | $ node my-module.js 167 | ``` 168 | 169 | 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. 170 | 171 | ```sh 172 | $ node --module my-module.js 173 | ``` 174 | 175 | ## Lookup Algorithm Psuedo-Code 176 | 177 | ### LOAD_MODULE(X, Y, T) 178 | 179 | Loads _X_ from a module at path _Y_. _T_ is either "require" or "import". 180 | 181 | 1. If X is a core module, then 182 | 1. return the core module 183 | 1. STOP 184 | 1. If X begins with './' or '/' or '../' 185 | 1. LOAD_AS_FILE(Y + X, T) 186 | 1. LOAD_AS_DIRECTORY(Y + X, T) 187 | 1. LOAD_NODE_MODULES(X, dirname(Y), T) 188 | 1. THROW "not found" 189 | 190 | ### LOAD_AS_FILE(X, T) 191 | 192 | 1. If T is "import", 193 | 1. If X is a file, then 194 | 1. If extname(X) is ".js", load X as ES module text. STOP 195 | 1. If extname(X) is ".json", parse X to a JavaScript Object. STOP 196 | 1. If extname(X) is ".node", load X as binary addon. STOP 197 | 1. THROW "not found" 198 | 1. Else, 199 | 1. Assert: T is "require" 200 | 1. If X is a file, load X as CJS module text. STOP 201 | 1. If X.js is a file, load X.js as CJS module text. STOP 202 | 1. If X.json is a file, parse X.json to a JavaScript Object. STOP 203 | 1. If X.node is a file, load X.node as binary addon. STOP 204 | 205 | ### LOAD_AS_DIRECTORY(X, T) 206 | 207 | 1. If T is "import", 208 | 1. If X/default.js is a file, load X/default.js as ES module text. STOP 209 | 1. If X/package.json is a file, 210 | 1. Parse X/package.json, and look for "default" field. 211 | 1. load X/(json module field) as ES module text. STOP 212 | 1. NOTE: If neither of the above are a file, then fallback to legacy behavior 213 | 1. If X/package.json is a file, 214 | 1. Parse X/package.json, and look for "main" field. 215 | 1. let M = X + (json main field) 216 | 1. LOAD_AS_FILE(M, "require") 217 | 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP 218 | 1. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP 219 | 1. If X/index.node is a file, load X/index.node as binary addon. STOP 220 | 221 | ### LOAD_NODE_MODULES(X, START, T) 222 | 223 | 1. let DIRS=NODE_MODULES_PATHS(START) 224 | 2. for each DIR in DIRS: 225 | 1. LOAD_AS_FILE(DIR/X, T) 226 | 1. LOAD_AS_DIRECTORY(DIR/X, T) 227 | --------------------------------------------------------------------------------