Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Resolve hook prototype #1

Open
wants to merge 8 commits into
base: initial-pr
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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');
let { 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;
}

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);
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