Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ coverage-build: all
"$(CURDIR)/testing/coverage/gcovr-patches.diff"); fi
if [ -d lib_ ]; then $(RM) -r lib; mv lib_ lib; fi
mv lib lib_
$(NODE) ./node_modules/.bin/nyc instrument lib_/ lib/
$(NODE) ./node_modules/.bin/nyc instrument --extension .js --extension .mjs lib_/ lib/
$(MAKE)

coverage-test: coverage-build
Expand Down Expand Up @@ -888,6 +888,8 @@ jslint:
@echo "Running JS linter..."
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --ext=.js,.md \
$(JSLINT_TARGETS)
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --parser-options=sourceType:module --ext=.mjs \
$(JSLINT_TARGETS)

jslint-ci:
@echo "Running JS linter..."
Expand Down
86 changes: 86 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ECMAScript Modules

<!--introduced_in=v9.x.x-->

> Stability: 1 - Experimental

<!--name=esm-->

Node contains support for ES Modules based upon [the Node EP for ES Modules](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md).

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.

## Enabling

<!-- type=misc -->

The `--experimental-modules` flag can be used to enable features for loading ESM modules.

Once this has been set, files ending with `.mjs` will be able to be loaded as ES Modules.

```sh
node --experimental-modules my-app.mjs
```

## Features

<!-- type=misc -->

### Supported

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.

### Unsupported

| Feature | Reason |
| --- | --- |
| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use language standard `import()` |
| `import()` | pending newer V8 release used in Node.js |
| `import.meta` | pending V8 implementation |
| Loader Hooks | pending Node.js EP creation/consensus |

## Notable differences between `import` and `require`

### No NODE_PATH

`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks if this behavior is desired.

### No `require.extensions`

`require.extensions` is not used by `import`. The expectation is that loader hooks can provide this workflow in the future.

### No `require.cache`

`require.cache` is not used by `import`. It has a separate cache.

### URL based paths

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.

Modules will be loaded multiple times if the `import` specifier used to resolve them have a different query or fragment.

```js
import "./foo?query=1"; // loads ./foo with query of "?query=1"
import "./foo?query=2"; // loads ./foo with query of "?query=2"
```

For now, only modules using the `file:` protocol can be loaded.

## Interop with existing modules

All CommonJS, JSON, and C++ modules can be used with `import`.

Modules loaded this way will only be loaded once, even if their query or fragment string differs between `import` statements.

When loaded via `import` these modules will provide a single `default` export representing the value of `module.exports` at the time they finished evaluating.

```js
import fs from 'fs';
fs.readFile('./foo.txt', (err, body) {
if (err) {
console.error(err);
} else {
console.log(body);
}
});
```
7 changes: 7 additions & 0 deletions lib/internal/bootstrap_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@
'DeprecationWarning', 'DEP0062', startup, true);
}

if (!!process.binding('config').experimentalModules) {
process.emitWarning(
'The ESM module loader is experimental.',
'ExperimentalWarning', undefined);
}


// There are various modes that Node can run in. The most common two
// are running from a script and running the REPL - but there are a few
// others like the debugger or running --eval arguments. Here we decide
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ E('ERR_INVALID_OPT_VALUE',
});
E('ERR_INVALID_OPT_VALUE_ENCODING',
(value) => `The value "${String(value)}" is invalid for option "encoding"`);
E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set');
E('ERR_INVALID_PROTOCOL', (protocol, expectedProtocol) =>
`Protocol "${protocol}" not supported. Expected "${expectedProtocol}"`);
E('ERR_INVALID_REPL_EVAL_CONFIG',
Expand All @@ -226,14 +227,17 @@ E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe');
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks');
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented');
E('ERR_MISSING_ARGS', missingArgs);
E('ERR_MISSING_MODULE', 'Cannot find module %s');
E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' +
'Legacy behavior in require would have found it at %s');
E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times');
E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function');
E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object');
E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support');
E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported');
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set');
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');
E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
E('ERR_SOCKET_BAD_TYPE',
Expand Down Expand Up @@ -270,6 +274,7 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
'At least one valid performance entry type is required');
E('ERR_VALUE_OUT_OF_RANGE', 'The value of "%s" must be %s. Received "%s"');


function invalidArgType(name, expected, actual) {
assert(name, 'name is required');

Expand Down
83 changes: 83 additions & 0 deletions lib/internal/loader/Loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const { URL } = require('url');
const { getURLFromFilePath } = require('internal/url');

const {
getNamespaceOfModuleWrap
} = require('internal/loader/ModuleWrap');

const ModuleMap = require('internal/loader/ModuleMap');
const ModuleJob = require('internal/loader/ModuleJob');
const { formatProviders, resolve } = require('internal/loader/ModuleRequest');
const errors = require('internal/errors');

function getBase() {
try {
return getURLFromFilePath(`${process.cwd()}/`);
} catch (e) {
e.stack;
// If the current working directory no longer exists.
if (e.code === 'ENOENT') {
return undefined;
}
throw e;
}
}

class Loader {
constructor(base = getBase()) {
this.moduleMap = new ModuleMap();
if (typeof base !== 'undefined' && base instanceof URL !== true) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'URL');
}
this.base = base;
}

setModuleResolver(resolver) {
resolve = resolver;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accidental assignment to a const?

}

async resolve(specifier, parentURLOrString = this.base) {
if (typeof parentURLOrString === 'string') {
parentURLOrString = new URL(parentURLOrString);
}
else if (parentURLOrString instanceof URL === false) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURLOrString', 'URL');
}
const { url, format } = await resolve(specifier, parentURLOrString);
if (typeof url === 'string') {
url = new URL(url);
}
else if (!(url instanceof URL)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'URL');
}
if (url.protocol !== 'file:') {
throw new errors.Error('ERR_INVALID_PROTOCOL',
request.url.protocol, 'file:');
}
if (!formatProviders.has(format)) {
throw new errors.Error('ERR_INVALID_FORMAT', format);
}
return { url, format };
}

async getModuleJob(specifier, parentURLOrString = this.base) {
const { url, format } = await this.resolve(specifier, parentURLOrString);
const urlString = `${url}`;
let job = this.moduleMap.get(urlString);
Copy link
Owner

@bmeck bmeck Sep 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check using .has

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done as a perf optimization to avoid repeated unnecessary dictionary lookups, but unsure how much it would affect things. Happy to change it.

if (job === undefined) {
job = new ModuleJob(this, url, formatProviders.get(format));
this.moduleMap.set(urlString, job);
}
return job;
}

async import(specifier, parentURLOrString = this.base) {
const job = await this.getModuleJob(specifier, parentURLOrString);
const module = await job.run();
return getNamespaceOfModuleWrap(module);
}
}
Object.setPrototypeOf(Loader.prototype, null);
module.exports = Loader;
114 changes: 114 additions & 0 deletions lib/internal/loader/ModuleJob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict';

const { SafeSet, SafePromise } = require('internal/safe_globals');
const resolvedPromise = SafePromise.resolve();
const resolvedArrayPromise = SafePromise.resolve([]);
const { ModuleWrap } = require('internal/loader/ModuleWrap');

class ModuleJob {
/**
* @param {module: ModuleWrap?, compiled: Promise} moduleProvider
*/
constructor(loader, url, moduleProvider) {
this.url = url;
this.loader = loader;
this.error = null;
this.hadError = false;

if (moduleProvider instanceof ModuleWrap !== true) {
// linked == promise for dependency jobs, with module populated,
// module wrapper linked
this.moduleProvider = moduleProvider;
this.modulePromise = this.moduleProvider(url);
this.module = undefined;
const linked = async () => {
const dependencyJobs = [];
this.module = await this.modulePromise;
this.module.link(async (dependencySpecifier) => {
const dependencyJobPromise =
this.loader.getModuleJob(dependencySpecifier, url);
dependencyJobs.push(dependencyJobPromise);
const dependencyJob = await dependencyJobPromise;
return dependencyJob.modulePromise;
});
return SafePromise.all(dependencyJobs);
};
this.linked = linked();

// instantiated == deep dependency jobs wrappers instantiated,
//module wrapper instantiated
this.instantiated = undefined;
} else {
this.moduleProvider = async () => moduleProvider;
this.modulePromise = this.moduleProvider();
this.module = moduleProvider;
this.linked = resolvedArrayPromise;
this.instantiated = this.modulePromise;
}
}

instantiate() {
if (this.instantiated) {
return this.instantiated;
}
return this.instantiated = new Promise(async (resolve, reject) => {
const jobsInGraph = new SafeSet();
let jobsReadyToInstantiate = 0;
// (this must be sync for counter to work)
const queueJob = (moduleJob) => {
if (jobsInGraph.has(moduleJob)) {
return;
}
jobsInGraph.add(moduleJob);
moduleJob.linked.then((dependencyJobs) => {
for (const dependencyJob of dependencyJobs) {
queueJob(dependencyJob);
}
checkComplete();
}, (e) => {
if (!this.hadError) {
this.error = e;
this.hadError = true;
}
checkComplete();
});
};
const checkComplete = () => {
if (++jobsReadyToInstantiate === jobsInGraph.size) {
// I believe we only throw once the whole tree is finished loading?
// or should the error bail early, leaving entire tree to still load?
if (this.hadError) {
reject(this.error);
} else {
try {
this.module.instantiate();
for (const dependencyJob of jobsInGraph) {
dependencyJob.instantiated = resolvedPromise;
}
resolve(this.module);
} catch (e) {
e.stack;
reject(e);
}
}
}
};
queueJob(this);
});
}

async run() {
const module = await this.instantiate();
try {
module.evaluate();
} catch (e) {
e.stack;
this.hadError = true;
this.error = e;
throw e;
}
return module;
}
}
Object.setPrototypeOf(ModuleJob.prototype, null);
module.exports = ModuleJob;
33 changes: 33 additions & 0 deletions lib/internal/loader/ModuleMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const ModuleJob = require('internal/loader/ModuleJob');
const { SafeMap } = require('internal/safe_globals');
const debug = require('util').debuglog('esm');
const errors = require('internal/errors');

// Tracks the state of the loader-level module cache
class ModuleMap extends SafeMap {
get(url) {
if (typeof url !== 'string') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
}
return super.get(url);
}
set(url, job) {
if (typeof url !== 'string') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
}
if (job instanceof ModuleJob !== true) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'job', 'ModuleJob');
}
debug(`Storing ${url} in ModuleMap`);
return super.set(url, job);
}
has(url) {
if (typeof url !== 'string') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
}
return super.has(url);
}
}
module.exports = ModuleMap;
Loading