Skip to content

Commit 865a3c3

Browse files
bmeckMylesBorins
authored andcommitted
module: Allow runMain to be ESM
This follows the EPS an allows the node CLI to have ESM as an entry point. `node ./example.mjs`. A newer V8 is needed for `import()` so that is not included. `import.meta` is still in specification stage so that also is not included. PR-URL: #14369 Author: Bradley Farias <[email protected]> Author: Guy Bedford <[email protected]> Author: Jan Krems <[email protected]> Author: Timothy Gu <[email protected]> Author: Michaël Zasso <[email protected]> Author: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent e3d0ff9 commit 865a3c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1578
-40
lines changed

.eslintrc.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ env:
1010
parserOptions:
1111
ecmaVersion: 2017
1212

13+
overrides:
14+
- files: ["doc/api/esm.md", "*.mjs"]
15+
parserOptions:
16+
sourceType: module
17+
1318
rules:
1419
# Possible Errors
1520
# http://eslint.org/docs/rules/#possible-errors

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ coverage-build: all
151151
"$(CURDIR)/testing/coverage/gcovr-patches.diff"); fi
152152
if [ -d lib_ ]; then $(RM) -r lib; mv lib_ lib; fi
153153
mv lib lib_
154-
$(NODE) ./node_modules/.bin/nyc instrument lib_/ lib/
154+
$(NODE) ./node_modules/.bin/nyc instrument --extension .js --extension .mjs lib_/ lib/
155155
$(MAKE)
156156

157157
coverage-test: coverage-build
@@ -887,7 +887,7 @@ JSLINT_TARGETS = benchmark doc lib test tools
887887

888888
jslint:
889889
@echo "Running JS linter..."
890-
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --ext=.js,.md \
890+
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --ext=.js,.mjs,.md \
891891
$(JSLINT_TARGETS)
892892

893893
jslint-ci:

doc/api/esm.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ECMAScript Modules
2+
3+
<!--introduced_in=v9.x.x-->
4+
5+
> Stability: 1 - Experimental
6+
7+
<!--name=esm-->
8+
9+
Node contains support for ES Modules based upon the [the Node EP for ES Modules][].
10+
11+
Not all features of the EP are complete and will be landing as both VM support and implementation is ready. Error messages are still being polished.
12+
13+
## Enabling
14+
15+
<!-- type=misc -->
16+
17+
The `--experimental-modules` flag can be used to enable features for loading ESM modules.
18+
19+
Once this has been set, files ending with `.mjs` will be able to be loaded as ES Modules.
20+
21+
```sh
22+
node --experimental-modules my-app.mjs
23+
```
24+
25+
## Features
26+
27+
<!-- type=misc -->
28+
29+
### Supported
30+
31+
Only the CLI argument for the main entry point to the program can be an entry point into an ESM graph. In the future `import()` can be used to create entry points into ESM graphs at run time.
32+
33+
### Unsupported
34+
35+
| Feature | Reason |
36+
| --- | --- |
37+
| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use language standard `import()` |
38+
| `import()` | pending newer V8 release used in Node.js |
39+
| `import.meta` | pending V8 implementation |
40+
| Loader Hooks | pending Node.js EP creation/consensus |
41+
42+
## Notable differences between `import` and `require`
43+
44+
### No NODE_PATH
45+
46+
`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks if this behavior is desired.
47+
48+
### No `require.extensions`
49+
50+
`require.extensions` is not used by `import`. The expectation is that loader hooks can provide this workflow in the future.
51+
52+
### No `require.cache`
53+
54+
`require.cache` is not used by `import`. It has a separate cache.
55+
56+
### URL based paths
57+
58+
ESM are resolved and cached based upon [URL](url.spec.whatwg.org) semantics. This means that files containing special characters such as `#` and `?` need to be escaped.
59+
60+
Modules will be loaded multiple times if the `import` specifier used to resolve them have a different query or fragment.
61+
62+
```js
63+
import './foo?query=1'; // loads ./foo with query of "?query=1"
64+
import './foo?query=2'; // loads ./foo with query of "?query=2"
65+
```
66+
67+
For now, only modules using the `file:` protocol can be loaded.
68+
69+
## Interop with existing modules
70+
71+
All CommonJS, JSON, and C++ modules can be used with `import`.
72+
73+
Modules loaded this way will only be loaded once, even if their query or fragment string differs between `import` statements.
74+
75+
When loaded via `import` these modules will provide a single `default` export representing the value of `module.exports` at the time they finished evaluating.
76+
77+
```js
78+
import fs from 'fs';
79+
fs.readFile('./foo.txt', (err, body) => {
80+
if (err) {
81+
console.error(err);
82+
} else {
83+
console.log(body);
84+
}
85+
});
86+
```
87+
88+
[the Node EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md

lib/internal/bootstrap_node.js

+7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@
109109
'DeprecationWarning', 'DEP0062', startup, true);
110110
}
111111

112+
if (process.binding('config').experimentalModules) {
113+
process.emitWarning(
114+
'The ESM module loader is experimental.',
115+
'ExperimentalWarning', undefined);
116+
}
117+
118+
112119
// There are various modes that Node can run in. The most common two
113120
// are running from a script and running the REPL - but there are a few
114121
// others like the debugger or running --eval arguments. Here we decide

lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,15 @@ E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected');
211211
E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe');
212212
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks');
213213
E('ERR_MISSING_ARGS', missingArgs);
214+
E('ERR_MISSING_MODULE', 'Cannot find module %s');
215+
E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' +
216+
'Legacy behavior in require would have found it at %s');
214217
E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function');
215218
E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object');
216219
E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support');
217220
E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
218221
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
222+
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
219223
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');
220224
E('ERR_SOCKET_BAD_TYPE',
221225
'Bad socket type specified. Valid types are: udp4, udp6');

lib/internal/loader/Loader.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
const { URL } = require('url');
4+
const { getURLFromFilePath } = require('internal/url');
5+
6+
const {
7+
getNamespaceOfModuleWrap
8+
} = require('internal/loader/ModuleWrap');
9+
10+
const ModuleMap = require('internal/loader/ModuleMap');
11+
const ModuleJob = require('internal/loader/ModuleJob');
12+
const resolveRequestUrl = require('internal/loader/resolveRequestUrl');
13+
const errors = require('internal/errors');
14+
15+
function getBase() {
16+
try {
17+
return getURLFromFilePath(`${process.cwd()}/`);
18+
} catch (e) {
19+
e.stack;
20+
// If the current working directory no longer exists.
21+
if (e.code === 'ENOENT') {
22+
return undefined;
23+
}
24+
throw e;
25+
}
26+
}
27+
28+
class Loader {
29+
constructor(base = getBase()) {
30+
this.moduleMap = new ModuleMap();
31+
if (typeof base !== 'undefined' && base instanceof URL !== true) {
32+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'URL');
33+
}
34+
this.base = base;
35+
}
36+
37+
async resolve(specifier) {
38+
const request = resolveRequestUrl(this.base, specifier);
39+
if (request.url.protocol !== 'file:') {
40+
throw new errors.Error('ERR_INVALID_PROTOCOL',
41+
request.url.protocol, 'file:');
42+
}
43+
return request.url;
44+
}
45+
46+
async getModuleJob(dependentJob, specifier) {
47+
if (!this.moduleMap.has(dependentJob.url)) {
48+
throw new errors.Error('ERR_MISSING_MODULE', dependentJob.url);
49+
}
50+
const request = await resolveRequestUrl(dependentJob.url, specifier);
51+
const url = `${request.url}`;
52+
if (this.moduleMap.has(url)) {
53+
return this.moduleMap.get(url);
54+
}
55+
const dependencyJob = new ModuleJob(this, request);
56+
this.moduleMap.set(url, dependencyJob);
57+
return dependencyJob;
58+
}
59+
60+
async import(specifier) {
61+
const request = await resolveRequestUrl(this.base, specifier);
62+
const url = `${request.url}`;
63+
let job;
64+
if (this.moduleMap.has(url)) {
65+
job = this.moduleMap.get(url);
66+
} else {
67+
job = new ModuleJob(this, request);
68+
this.moduleMap.set(url, job);
69+
}
70+
const module = await job.run();
71+
return getNamespaceOfModuleWrap(module);
72+
}
73+
}
74+
Object.setPrototypeOf(Loader.prototype, null);
75+
module.exports = Loader;

lib/internal/loader/ModuleJob.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const { SafeSet, SafePromise } = require('internal/safe_globals');
4+
const resolvedPromise = SafePromise.resolve();
5+
const resolvedArrayPromise = SafePromise.resolve([]);
6+
const { ModuleWrap } = require('internal/loader/ModuleWrap');
7+
8+
const NOOP = () => { /* No-op */ };
9+
class ModuleJob {
10+
/**
11+
* @param {module: ModuleWrap?, compiled: Promise} moduleProvider
12+
*/
13+
constructor(loader, moduleProvider, url) {
14+
this.url = `${moduleProvider.url}`;
15+
this.moduleProvider = moduleProvider;
16+
this.loader = loader;
17+
this.error = null;
18+
this.hadError = false;
19+
20+
if (moduleProvider instanceof ModuleWrap !== true) {
21+
// linked == promise for dependency jobs, with module populated,
22+
// module wrapper linked
23+
this.modulePromise = this.moduleProvider.createModule();
24+
this.module = undefined;
25+
const linked = async () => {
26+
const dependencyJobs = [];
27+
this.module = await this.modulePromise;
28+
this.module.link(async (dependencySpecifier) => {
29+
const dependencyJobPromise =
30+
this.loader.getModuleJob(this, dependencySpecifier);
31+
dependencyJobs.push(dependencyJobPromise);
32+
const dependencyJob = await dependencyJobPromise;
33+
return dependencyJob.modulePromise;
34+
});
35+
return SafePromise.all(dependencyJobs);
36+
};
37+
this.linked = linked();
38+
39+
// instantiated == deep dependency jobs wrappers instantiated,
40+
//module wrapper instantiated
41+
this.instantiated = undefined;
42+
} else {
43+
const getModuleProvider = async () => moduleProvider;
44+
this.modulePromise = getModuleProvider();
45+
this.moduleProvider = { finish: NOOP };
46+
this.module = moduleProvider;
47+
this.linked = resolvedArrayPromise;
48+
this.instantiated = this.modulePromise;
49+
}
50+
}
51+
52+
instantiate() {
53+
if (this.instantiated) {
54+
return this.instantiated;
55+
}
56+
return this.instantiated = new Promise(async (resolve, reject) => {
57+
const jobsInGraph = new SafeSet();
58+
let jobsReadyToInstantiate = 0;
59+
// (this must be sync for counter to work)
60+
const queueJob = (moduleJob) => {
61+
if (jobsInGraph.has(moduleJob)) {
62+
return;
63+
}
64+
jobsInGraph.add(moduleJob);
65+
moduleJob.linked.then((dependencyJobs) => {
66+
for (const dependencyJob of dependencyJobs) {
67+
queueJob(dependencyJob);
68+
}
69+
checkComplete();
70+
}, (e) => {
71+
if (!this.hadError) {
72+
this.error = e;
73+
this.hadError = true;
74+
}
75+
checkComplete();
76+
});
77+
};
78+
const checkComplete = () => {
79+
if (++jobsReadyToInstantiate === jobsInGraph.size) {
80+
// I believe we only throw once the whole tree is finished loading?
81+
// or should the error bail early, leaving entire tree to still load?
82+
if (this.hadError) {
83+
reject(this.error);
84+
} else {
85+
try {
86+
this.module.instantiate();
87+
for (const dependencyJob of jobsInGraph) {
88+
dependencyJob.instantiated = resolvedPromise;
89+
}
90+
resolve(this.module);
91+
} catch (e) {
92+
e.stack;
93+
reject(e);
94+
}
95+
}
96+
}
97+
};
98+
queueJob(this);
99+
});
100+
}
101+
102+
async run() {
103+
const module = await this.instantiate();
104+
try {
105+
module.evaluate();
106+
} catch (e) {
107+
e.stack;
108+
this.hadError = true;
109+
this.error = e;
110+
throw e;
111+
}
112+
return module;
113+
}
114+
}
115+
Object.setPrototypeOf(ModuleJob.prototype, null);
116+
module.exports = ModuleJob;

lib/internal/loader/ModuleMap.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
const ModuleJob = require('internal/loader/ModuleJob');
4+
const { SafeMap } = require('internal/safe_globals');
5+
const debug = require('util').debuglog('esm');
6+
const errors = require('internal/errors');
7+
8+
// Tracks the state of the loader-level module cache
9+
class ModuleMap extends SafeMap {
10+
get(url) {
11+
if (typeof url !== 'string') {
12+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
13+
}
14+
return super.get(url);
15+
}
16+
set(url, job) {
17+
if (typeof url !== 'string') {
18+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
19+
}
20+
if (job instanceof ModuleJob !== true) {
21+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'job', 'ModuleJob');
22+
}
23+
debug(`Storing ${url} in ModuleMap`);
24+
return super.set(url, job);
25+
}
26+
has(url) {
27+
if (typeof url !== 'string') {
28+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
29+
}
30+
return super.has(url);
31+
}
32+
}
33+
module.exports = ModuleMap;

0 commit comments

Comments
 (0)