|
| 1 | +| Title | Package.json Module Property | |
| 2 | +|--------|------------------------------| |
| 3 | +| Author | @guybedford | |
| 4 | +| Status | DRAFT | |
| 5 | +| Date | 2017-07-13 | |
| 6 | + |
| 7 | +## 1. Background |
| 8 | + |
| 9 | +This proposal specifies the `"module"` property in the package.json, building |
| 10 | +on the previous work in the |
| 11 | +[In Defense of Dot JS](https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md) |
| 12 | +proposal (DDJS), as well as many other discussions. |
| 13 | + |
| 14 | +Instead of supporting the additional `"modules"` and `"modules.root"` |
| 15 | +properties from that proposal, this proposal aims to adjust the handling of |
| 16 | +`"module"` slightly so that it is the only property supported. |
| 17 | + |
| 18 | +A draft specification of the NodeJS module resolution algorithm with this |
| 19 | +adjustment is included in section 5. Draft Specification. |
| 20 | + |
| 21 | +## 2. Motivation |
| 22 | + |
| 23 | +There is still uncertainty as to how exactly to distinguish an ES Module from |
| 24 | +a CommonJS module. While `.mjs` and `"use module"` |
| 25 | +(https://github.com/tc39/proposal-modules-pragma) act as useful indicators, |
| 26 | +these properties act as file-specific indicators of the module format. If we |
| 27 | +are to keep the `.js` extension without making `"use module"` mandatory, then |
| 28 | +there is also a need for a more global indication that a package contains only |
| 29 | +ES modules. |
| 30 | + |
| 31 | +Currently all our JS build tools detect modules in slightly different ways. |
| 32 | +The `package.json` `module` property has gained good traction as an entry point |
| 33 | +mechanism, but there isn't currently clarity on how exactly this property |
| 34 | +behaves in the edge cases and for submodule requires (`pkg/x` imports). Since |
| 35 | +tools are currently driving the ecosystem conventions, it is worth refining the |
| 36 | +exact conventions with an active specification that can gain support, so that |
| 37 | +we can continue to converge on the module contract in NodeJS, and do our best |
| 38 | +to avoid incompatibilities in future. |
| 39 | + |
| 40 | +## 4. Proposal |
| 41 | + |
| 42 | +Instead of trying to consider a single unified resolver, we break the behaviour |
| 43 | +of NodeJS resolution into two separate resolvers: |
| 44 | +* The current resolver as in use today, which will continue to be used to |
| 45 | +resolve CommonJS modules from CommonJS modules, to ensure absolutely no |
| 46 | +breaking edge cases. |
| 47 | +* The new ES Modules resolver, that also has the ability to load CommonJS |
| 48 | +modules based on a small extension to the existing resolution algorithm. |
| 49 | + |
| 50 | +When using CommonJS `require`, the legacy resolver would be applied, and when |
| 51 | +using ES modules, the new ES module resolver algorithm, as along the lines |
| 52 | +specified here would be applied. |
| 53 | + |
| 54 | +**The rule proposed here is that the ES module resolver always loads a module |
| 55 | +from package with a "module" property as an ES module, and loads a module from |
| 56 | +a package without that property as a CommonJS module (unless it is a .mjs file |
| 57 | +or "use module" source).** |
| 58 | + |
| 59 | +Under this rule, the simple cases remain the same as the DDJS proposal: |
| 60 | + |
| 61 | +* A package with only a `main` and no `module` property will be loaded as |
| 62 | +containing CommonJS modules only. |
| 63 | +* A package with only a `module` property and no `main` property will be loaded |
| 64 | +as containing ES Modules only. |
| 65 | + |
| 66 | +The difficult case with the DDJS proposal is the transition case of a package |
| 67 | +that contains both a `main` and `module` property - selecting which main entry |
| 68 | +point and target to use when loading `pkg` or `pkg/x.js`. |
| 69 | + |
| 70 | +For a package that contains both a `main` and a `module` property - |
| 71 | +* When the parent module doing the require is an ES Module, the `module` main |
| 72 | +will apply, and any module loaded from the package will be loaded as an ES Module. |
| 73 | +* When the parent module doing the require is a CommonJS module, the `main` |
| 74 | +main will apply, and any module loaded from the package will be loaded as |
| 75 | +a CommonJS Module. |
| 76 | + |
| 77 | +In this way, we continue to support the existing ecosystem with backwards |
| 78 | +compatibility, while keeping the scope of the specification as simple as possible. |
| 79 | + |
| 80 | +## 4.1 Public API for Mixed CJS and ES Module Packages |
| 81 | + |
| 82 | +A package delivering both CommonJS and ES Modules would then typically |
| 83 | +tell its users to just import via `import {x} from 'pkgName'` or |
| 84 | +`require('pkgName').x`, with the `module` and `main` properties applying |
| 85 | +respectively. |
| 86 | + |
| 87 | +In the case where a package publicly exposes sub-modules, it would need |
| 88 | +to document that the CommonJS and ES Module sources are at different paths - |
| 89 | +`import {x} from 'pkgName/submodule.js'` and |
| 90 | +`import {x} from 'pkgName/cjs/submodule.js'`. Or simply a `.js` and |
| 91 | + `.mjs` variant, this being the author's preference. |
| 92 | + |
| 93 | +## 4.2 Package Boundary Detection |
| 94 | + |
| 95 | +This proposal, like DDJS, requires that we can get the package configuration |
| 96 | +given only a module path. This is based on checking the package.json file |
| 97 | +through the folder hierarchy: |
| 98 | + |
| 99 | +* For a given module, the package.json file is checked in that folder, |
| 100 | +continuing to check parent folders for a package.json if none is found. If we |
| 101 | +reach a parent folder of `node_modules`, we stop this search process. |
| 102 | +* When no package.json module property is found, NodeJS would default to |
| 103 | +loading any module as CommonJS. |
| 104 | + |
| 105 | +These rules are taken into account in the draft specification included below. |
| 106 | + |
| 107 | +## 4.3 Loading Modules without a package.json |
| 108 | + |
| 109 | +If writing a `.js` file without any `package.json` configuration, it remains |
| 110 | +possible to opt-in to ES modules by indicating this by either using the `.mjs` |
| 111 | +file extension or `"use module"` directive, which always take preference. |
| 112 | + |
| 113 | +## 4.4 Packages Consisting of both CommonJS and ES Modules |
| 114 | + |
| 115 | +For a package that contains both ES modules in a `lib` folder and CommonJS |
| 116 | +modules in a `test` folder, if it was desired to be able to load both formats |
| 117 | +with the NodeJS ES Module resolver, the approach that could be taken would be |
| 118 | +to have two package.json files - one at the base of the package with a |
| 119 | +package.json containing a `module` property, and another in the `test` folder |
| 120 | +itself, without any `module` property. The `test` folder package.json would |
| 121 | +then take precedence for that subfolder, allowing a partial adoption path. |
| 122 | + |
| 123 | +While this approach is by no means elegant, it falls out as a side effect of |
| 124 | +the package detection, and provides an adequate workaround for the transition |
| 125 | +phase. |
| 126 | + |
| 127 | +## 4.5 Caching |
| 128 | + |
| 129 | +For performance the package.json contents are cached for the duration of |
| 130 | +execution (including caching the absence of a package.json file), just like |
| 131 | +modules get cached in the module registry for the duration of execution. This |
| 132 | +caching behaviour is described in the draft specification here. |
| 133 | + |
| 134 | +## 4.6 Enabling wasm |
| 135 | + |
| 136 | +For future support of Web Assembly, this spec also reserves the file extension |
| 137 | +`.wasm` as throwing an error when attempting to load modules with this |
| 138 | +extension, in order to allow Web Assembly loading to work by default in future. |
| 139 | + |
| 140 | +# 5. Draft Specification |
| 141 | + |
| 142 | +The `RESOLVE` function specified here specifies the ES Module resolver used |
| 143 | +only for ES Module specifier resolution, separate to the existing `require()` |
| 144 | +resolver. |
| 145 | + |
| 146 | +It is specified here to return a `Module` object, which would effectively be a |
| 147 | +wrapper of the |
| 148 | +[V8 Module class](https://v8.paulfryzel.com/docs/master/classv8_1_1_module.html). |
| 149 | + |
| 150 | +> **RESOLVE(name: String, parentPath: String): Module** |
| 151 | +> 1. Assert _parentPath_ is a valid file system path. |
| 152 | +> 1. If _name_ is a NodeJS core module then, |
| 153 | +> 1. Return the NodeJS core _Module_ object. |
| 154 | +> 1. If _name_ is a valid absolute file system path, or begins with _'./'_, |
| 155 | +_'/'_ or '../' then, |
| 156 | +> 1. Let _requestPath_ be the path resolution of _name_ to _parentPath_, |
| 157 | +with URL percent-decoding applied and any _"\\"_ characters converted into |
| 158 | +_"/"_ for posix environments. |
| 159 | +> 1. Return the result of _RESOLVE_MODULE_PATH(requestPath)_, propagating |
| 160 | +any error on abrupt completion. |
| 161 | +> 1. Otherwise, if _name_ parses as a _URL_ then, |
| 162 | +> 1. If _name_ is not a valid file system URL then, |
| 163 | +> 1. Throw _Invalid Module Name_. |
| 164 | +> 1. Let _requestPath_ be the file system path corresponding to the file |
| 165 | +URL. |
| 166 | +> 1. Return the result of _RESOLVE_MODULE_PATH(requestPath)_, propagating |
| 167 | +any error on abrupt completion. |
| 168 | +> 1. Otherwise, |
| 169 | +> 1. Return the result of _NODE_MODULES_RESOLVE(name)_, propagating any |
| 170 | +error on abrupt completion. |
| 171 | + |
| 172 | +> **RESOLVE_MODULE_PATH(requestPath: String): Module** |
| 173 | +> 1. Let _{ main, module, packagePath }_ be the destructured object values of |
| 174 | +the result of _GET_PACKAGE_CONFIG(requestPath)_, propagating any errors on |
| 175 | +abrupt completion. |
| 176 | +> 1. Let _loadAsModule_ be equal to _false_. |
| 177 | +> 1. If _module_ is equal to _true_ then, |
| 178 | +> 1. Set _main_ to _undefined_. |
| 179 | +> 1. Set _loadAsModule_ to _true_. |
| 180 | +> 1. If _module_ is a string then, |
| 181 | +> 1. Set _main_ to _module_. |
| 182 | +> 1. Set _loadAsModule_ to _true_. |
| 183 | +> 1. If _main_ is not _undefined_ and _packagePath_ is not _undefined_ and is |
| 184 | +equal to the path of _requestPath_ (ignoring trailing path separators) then, |
| 185 | +> 1. Set _requestPath_ to the path resolution of _main_ to _packagePath_. |
| 186 | +> 1. Let _resolvedPath_ be the result of _RESOLVE_FILE(requestPath)_, |
| 187 | +propagating any error on abrubt completion. |
| 188 | +> 1. If _resolvedPath_ is not _undefined_ then, |
| 189 | +> 1. If _resolvedPath_ ends with _".mjs"_ then, |
| 190 | +> 1. Return the resolved module at _resolvedPath_, loaded as an |
| 191 | +ECMAScript module. |
| 192 | +> 1. If _resolvedPath_ ends with _".json"_ then, |
| 193 | +> 1. Return the resolved module at _resolvedPath_, loaded as a JSON file. |
| 194 | +> 1. If _resolvedPath_ ends with _".node"_ then, |
| 195 | +> 1. Return the resolved module at _resolvedPath_, loaded as a NodeJS |
| 196 | +binary. |
| 197 | +> 1. If _resolvedPath_ ends with _".wasm"_ then, |
| 198 | +> 1. Throw _Invalid Module Name_. |
| 199 | +> 1. If _loadAsModule_ is set to _true_ then, |
| 200 | +> 1. Return the resolved module at _resolvedPath_, loaded as an |
| 201 | +ECMAScript module. |
| 202 | +> 1. If the module at _resolvedPath_ contains a _"use module"_ directive |
| 203 | +then, |
| 204 | +> 1. Return the resolved module at _resolvedPath_, loaded as an |
| 205 | +ECMAScript module. |
| 206 | +> 1. Otherwise, |
| 207 | +> 1. Return the resolved module at _resolvedPath_, loaded as a CommonJS |
| 208 | +module. |
| 209 | +> 1. Throw _Not Found_. |
| 210 | +
|
| 211 | +> **GET_PACKAGE_CONFIG(requestPath: String): { main: String, format: String, |
| 212 | +packagePath: String }** |
| 213 | +> 1. For each parent folder _packagePath_ of _requestPath_ in descending order |
| 214 | +of length, |
| 215 | +> 1. If there is already a cached package config result for _packagePath_ |
| 216 | +then, |
| 217 | +> 1. If that cached package result is an empty configuration entry then, |
| 218 | +> 1. Continue the loop. |
| 219 | +> 1. Otherwise, |
| 220 | +> 1. Return the cached package config result for this folder. |
| 221 | +> 1. If _packagePath_ ends with the segment _"node_modules"_ then, |
| 222 | +> 1. Break the loop. |
| 223 | +> 1. If _packagePath_ contains a _package.json_ file then, |
| 224 | +> 1. Let _json_ be the parsed JSON of the contents of the file at |
| 225 | +"${packagePath}/package.json", throwing an error for _Invalid JSON_. |
| 226 | +> 1. Let _main_ be the value of _json.main_. |
| 227 | +> 1. If _main_ is defined and not a string, throw _Invalid Config_. |
| 228 | +> 1. Let _module_ be the value of _json.module_. |
| 229 | +> 1. If _module_ is defined and not a string or boolean, throw _Invalid |
| 230 | +Config_. |
| 231 | +> 1. Let _result_ be the object with keys for the values of _{ main, |
| 232 | +module, packagePath }_. |
| 233 | +> 1. Set in the package config cache the value for _packagePath_ as |
| 234 | +_result_. |
| 235 | +> 1. Return _result_. |
| 236 | +> 1. Otherwise, |
| 237 | +> 1. Set in the package config cache the value for _packagePath_ as an |
| 238 | +empty configuration entry. |
| 239 | +> 1. Return the empty configuration object _{ main: undefined, module: |
| 240 | +undefined, packagePath: undefined }_. |
| 241 | + |
| 242 | +> **RESOLVE_FILE(filePath: String): String** |
| 243 | +> 1. If _filePath_ is a file, return _X_. |
| 244 | +> 1. If _"${filePath}.mjs"_ is a file, return _"${filePath}.mjs"_. |
| 245 | +> 1. If _"${filePath}.js"_ is a file, return _"${filePath}.js"_. |
| 246 | +> 1. If _"${filePath}.json"_ is a file, return _"${filePath}.json"_. |
| 247 | +> 1. If _"${filePath}.node"_ is a file, return _"${filePath}.node"_. |
| 248 | +> 1. If _"${filePath}/index.js"_ is a file, return _"${filePath}/index.js"_. |
| 249 | +> 1. If _"${filePath}/index.json"_ is a file, return _"${filePath}/index.json"_. |
| 250 | +> 1. If _"${filePath}/index.node"_ is a file, return _"${filePath}/index.node"_. |
| 251 | +> 1. Return _undefined_. |
| 252 | +
|
| 253 | +> **NODE_MODULES_RESOLVE(name: String, parentPath: String): String** |
| 254 | +> 1. For each parent folder _modulesPath_ of _parentPath_ in descending order |
| 255 | +of length, |
| 256 | +> 1. Let _resolvedModule_ be the result of |
| 257 | +_RESOLVE_MODULE_PATH("${modulesPath}/node_modules/${name}/")_, propagating any |
| 258 | +errors on abrupt completion. |
| 259 | +> 1. If _resolvedModule_ is not _undefined_ then, |
| 260 | +> 1. Return _resolvedModule_. |
0 commit comments