Skip to content
This repository was archived by the owner on Jul 31, 2018. It is now read-only.

Commit 297b358

Browse files
committed
package module property specification
1 parent 54b199c commit 297b358

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

xxx-package-module-property.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)