Browse Source

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: https://github.com/nodejs/node/pull/14369
Author: Bradley Farias <bradley.meck@gmail.com>
Author: Guy Bedford <guybedford@gmail.com>
Author: Jan Krems <jan.krems@groupon.com>
Author: Timothy Gu <timothygu99@gmail.com>
Author: Michaël Zasso <targos@protonmail.com>
Author: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
canary-base
Bradley Farias 8 years ago
parent
commit
c8a389e19f
  1. 5
      .eslintrc.yaml
  2. 4
      Makefile
  3. 88
      doc/api/esm.md
  4. 7
      lib/internal/bootstrap_node.js
  5. 4
      lib/internal/errors.js
  6. 75
      lib/internal/loader/Loader.js
  7. 116
      lib/internal/loader/ModuleJob.js
  8. 33
      lib/internal/loader/ModuleMap.js
  9. 61
      lib/internal/loader/ModuleWrap.js
  10. 104
      lib/internal/loader/resolveRequestUrl.js
  11. 33
      lib/internal/loader/search.js
  12. 26
      lib/internal/safe_globals.js
  13. 7
      lib/internal/url.js
  14. 58
      lib/module.js
  15. 9
      node.gyp
  16. 531
      src/module_wrap.cc
  17. 58
      src/module_wrap.h
  18. 8
      src/node.cc
  19. 3
      src/node_config.cc
  20. 4
      src/node_internals.h
  21. 63
      src/node_url.cc
  22. 4
      src/node_url.h
  23. 25
      test/cctest/test_url.cc
  24. 7
      test/es-module/es-module.status
  25. 5
      test/es-module/esm-snapshot-mutator.js
  26. 3
      test/es-module/esm-snapshot.js
  27. 8
      test/es-module/test-esm-basic-imports.mjs
  28. 10
      test/es-module/test-esm-encoded-path-native.js
  29. 7
      test/es-module/test-esm-encoded-path.mjs
  30. 24
      test/es-module/test-esm-forbidden-globals.mjs
  31. 7
      test/es-module/test-esm-namespace.mjs
  32. 5
      test/es-module/test-esm-ok.mjs
  33. 8
      test/es-module/test-esm-pkg-over-ext.mjs
  34. 38
      test/es-module/test-esm-preserve-symlinks.js
  35. 7
      test/es-module/test-esm-require-cache.mjs
  36. 6
      test/es-module/test-esm-shebang.mjs
  37. 7
      test/es-module/test-esm-snapshot.mjs
  38. 48
      test/es-module/test-esm-symlink.js
  39. 6
      test/es-module/testcfg.py
  40. 2
      test/fixtures/es-module-require-cache/counter.js
  41. 1
      test/fixtures/es-module-require-cache/preload.js
  42. 0
      test/fixtures/es-module-url/empty.js
  43. 2
      test/fixtures/es-module-url/native.mjs
  44. 13
      test/testpy/__init__.py
  45. 60
      tools/eslint-rules/required-modules.js
  46. 18
      tools/test.py

5
.eslintrc.yaml

@ -10,6 +10,11 @@ env:
parserOptions:
ecmaVersion: 2017
overrides:
- files: ["doc/api/esm.md", "*.mjs"]
parserOptions:
sourceType: module
rules:
# Possible Errors
# http://eslint.org/docs/rules/#possible-errors

4
Makefile

@ -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
@ -886,7 +886,7 @@ JSLINT_TARGETS = benchmark doc lib test tools
jslint:
@echo "Running JS linter..."
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --ext=.js,.md \
$(NODE) tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules --ext=.js,.mjs,.md \
$(JSLINT_TARGETS)
jslint-ci:

88
doc/api/esm.md

@ -0,0 +1,88 @@
# ECMAScript Modules
<!--introduced_in=v9.x.x-->
> Stability: 1 - Experimental
<!--name=esm-->
Node contains support for ES Modules based upon the [the Node EP for ES Modules][].
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);
}
});
```
[the Node EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md

7
lib/internal/bootstrap_node.js

@ -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

4
lib/internal/errors.js

@ -229,6 +229,9 @@ 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');
@ -237,6 +240,7 @@ 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_OUTOFMEMORY', 'Out of memory');
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
E('ERR_SERVER_ALREADY_LISTEN',
'Listen method has been called more than once without closing.');
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');

75
lib/internal/loader/Loader.js

@ -0,0 +1,75 @@
'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 resolveRequestUrl = require('internal/loader/resolveRequestUrl');
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;
}
async resolve(specifier) {
const request = resolveRequestUrl(this.base, specifier);
if (request.url.protocol !== 'file:') {
throw new errors.Error('ERR_INVALID_PROTOCOL',
request.url.protocol, 'file:');
}
return request.url;
}
async getModuleJob(dependentJob, specifier) {
if (!this.moduleMap.has(dependentJob.url)) {
throw new errors.Error('ERR_MISSING_MODULE', dependentJob.url);
}
const request = await resolveRequestUrl(dependentJob.url, specifier);
const url = `${request.url}`;
if (this.moduleMap.has(url)) {
return this.moduleMap.get(url);
}
const dependencyJob = new ModuleJob(this, request);
this.moduleMap.set(url, dependencyJob);
return dependencyJob;
}
async import(specifier) {
const request = await resolveRequestUrl(this.base, specifier);
const url = `${request.url}`;
let job;
if (this.moduleMap.has(url)) {
job = this.moduleMap.get(url);
} else {
job = new ModuleJob(this, request);
this.moduleMap.set(url, job);
}
const module = await job.run();
return getNamespaceOfModuleWrap(module);
}
}
Object.setPrototypeOf(Loader.prototype, null);
module.exports = Loader;

116
lib/internal/loader/ModuleJob.js

@ -0,0 +1,116 @@
'use strict';
const { SafeSet, SafePromise } = require('internal/safe_globals');
const resolvedPromise = SafePromise.resolve();
const resolvedArrayPromise = SafePromise.resolve([]);
const { ModuleWrap } = require('internal/loader/ModuleWrap');
const NOOP = () => { /* No-op */ };
class ModuleJob {
/**
* @param {module: ModuleWrap?, compiled: Promise} moduleProvider
*/
constructor(loader, moduleProvider, url) {
this.url = `${moduleProvider.url}`;
this.moduleProvider = moduleProvider;
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.modulePromise = this.moduleProvider.createModule();
this.module = undefined;
const linked = async () => {
const dependencyJobs = [];
this.module = await this.modulePromise;
this.module.link(async (dependencySpecifier) => {
const dependencyJobPromise =
this.loader.getModuleJob(this, dependencySpecifier);
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 {
const getModuleProvider = async () => moduleProvider;
this.modulePromise = getModuleProvider();
this.moduleProvider = { finish: NOOP };
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
lib/internal/loader/ModuleMap.js

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

61
lib/internal/loader/ModuleWrap.js

@ -0,0 +1,61 @@
'use strict';
const { ModuleWrap } = process.binding('module_wrap');
const debug = require('util').debuglog('esm');
const ArrayJoin = Function.call.bind(Array.prototype.join);
const ArrayMap = Function.call.bind(Array.prototype.map);
const getNamespaceOfModuleWrap = (m) => {
const tmp = new ModuleWrap('import * as _ from "";_;', '');
tmp.link(async () => m);
tmp.instantiate();
return tmp.evaluate();
};
const createDynamicModule = (exports, url = '', evaluate) => {
debug(
`creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}`
);
const names = ArrayMap(exports, (name) => `${name}`);
// sanitized ESM for reflection purposes
const src = `export let executor;
${ArrayJoin(ArrayMap(names, (name) => `export let $${name}`), ';\n')}
;(() => [
fn => executor = fn,
{ exports: { ${
ArrayJoin(ArrayMap(names, (name) => `${name}: {
get: () => $${name},
set: v => $${name} = v
}`), ',\n')
} } }
]);
`;
const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`);
reflectiveModule.instantiate();
const [setExecutor, reflect] = reflectiveModule.evaluate()();
// public exposed ESM
const reexports = `import { executor,
${ArrayMap(names, (name) => `$${name}`)}
} from "";
export {
${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')}
}
// add await to this later if top level await comes along
typeof executor === "function" ? executor() : void 0;`;
if (typeof evaluate === 'function') {
setExecutor(() => evaluate(reflect));
}
const runner = new ModuleWrap(reexports, `${url}`);
runner.link(async () => reflectiveModule);
runner.instantiate();
return {
module: runner,
reflect
};
};
module.exports = {
createDynamicModule,
getNamespaceOfModuleWrap,
ModuleWrap
};

104
lib/internal/loader/resolveRequestUrl.js

@ -0,0 +1,104 @@
'use strict';
const { URL } = require('url');
const internalCJSModule = require('internal/module');
const internalURLModule = require('internal/url');
const internalFS = require('internal/fs');
const NativeModule = require('native_module');
const { extname } = require('path');
const { realpathSync } = require('fs');
const preserveSymlinks = !!process.binding('config').preserveSymlinks;
const {
ModuleWrap,
createDynamicModule
} = require('internal/loader/ModuleWrap');
const errors = require('internal/errors');
const search = require('internal/loader/search');
const asyncReadFile = require('util').promisify(require('fs').readFile);
const debug = require('util').debuglog('esm');
const realpathCache = new Map();
class ModuleRequest {
constructor(url) {
this.url = url;
}
}
Object.setPrototypeOf(ModuleRequest.prototype, null);
// Strategy for loading a standard JavaScript module
class StandardModuleRequest extends ModuleRequest {
async createModule() {
const source = `${await asyncReadFile(this.url)}`;
debug(`Loading StandardModule ${this.url}`);
return new ModuleWrap(internalCJSModule.stripShebang(source),
`${this.url}`);
}
}
// Strategy for loading a node-style CommonJS module
class CJSModuleRequest extends ModuleRequest {
async createModule() {
const ctx = createDynamicModule(['default'], this.url, (reflect) => {
debug(`Loading CJSModule ${this.url.pathname}`);
const CJSModule = require('module');
const pathname = internalURLModule.getPathFromURL(this.url);
CJSModule._load(pathname);
});
this.finish = (module) => {
ctx.reflect.exports.default.set(module.exports);
};
return ctx.module;
}
}
// Strategy for loading a node builtin CommonJS module that isn't
// through normal resolution
class NativeModuleRequest extends CJSModuleRequest {
async createModule() {
const ctx = createDynamicModule(['default'], this.url, (reflect) => {
debug(`Loading NativeModule ${this.url.pathname}`);
const exports = require(this.url.pathname);
reflect.exports.default.set(exports);
});
return ctx.module;
}
}
const normalizeBaseURL = (baseURLOrString) => {
if (baseURLOrString instanceof URL) return baseURLOrString;
if (typeof baseURLOrString === 'string') return new URL(baseURLOrString);
return undefined;
};
const resolveRequestUrl = (baseURLOrString, specifier) => {
if (NativeModule.nonInternalExists(specifier)) {
return new NativeModuleRequest(new URL(`node:${specifier}`));
}
const baseURL = normalizeBaseURL(baseURLOrString);
let url = search(specifier, baseURL);
if (url.protocol !== 'file:') {
throw new errors.Error('ERR_INVALID_PROTOCOL', url.protocol, 'file:');
}
if (!preserveSymlinks) {
const real = realpathSync(internalURLModule.getPathFromURL(url), {
[internalFS.realpathCacheKey]: realpathCache
});
const old = url;
url = internalURLModule.getURLFromFilePath(real);
url.search = old.search;
url.hash = old.hash;
}
const ext = extname(url.pathname);
if (ext === '.mjs') {
return new StandardModuleRequest(url);
}
return new CJSModuleRequest(url);
};
module.exports = resolveRequestUrl;

33
lib/internal/loader/search.js

@ -0,0 +1,33 @@
'use strict';
const { URL } = require('url');
const CJSmodule = require('module');
const errors = require('internal/errors');
const { resolve } = process.binding('module_wrap');
module.exports = (target, base) => {
target = `${target}`;
if (base === undefined) {
// We cannot search without a base.
throw new errors.Error('ERR_MISSING_MODULE', target);
}
base = `${base}`;
try {
return resolve(target, base);
} catch (e) {
e.stack; // cause V8 to generate stack before rethrow
let error = e;
try {
const questionedBase = new URL(base);
const tmpMod = new CJSmodule(questionedBase.pathname, null);
tmpMod.paths = CJSmodule._nodeModulePaths(
new URL('./', questionedBase).pathname);
const found = CJSmodule._resolveFilename(target, tmpMod);
error = new errors.Error('ERR_MODULE_RESOLUTION_LEGACY', target,
base, found);
} catch (problemChecking) {
// ignore
}
throw error;
}
};

26
lib/internal/safe_globals.js

@ -0,0 +1,26 @@
'use strict';
const copyProps = (unsafe, safe) => {
for (const key of [...Object.getOwnPropertyNames(unsafe),
...Object.getOwnPropertySymbols(unsafe)
]) {
if (!Object.getOwnPropertyDescriptor(safe, key)) {
Object.defineProperty(
safe,
key,
Object.getOwnPropertyDescriptor(unsafe, key));
}
}
};
const makeSafe = (unsafe, safe) => {
copyProps(unsafe.prototype, safe.prototype);
copyProps(unsafe, safe);
Object.setPrototypeOf(safe.prototype, null);
Object.freeze(safe.prototype);
Object.freeze(safe);
return safe;
};
exports.SafeMap = makeSafe(Map, class SafeMap extends Map {});
exports.SafeSet = makeSafe(Set, class SafeSet extends Set {});
exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {});

7
lib/internal/url.js

@ -1377,6 +1377,12 @@ function getPathFromURL(path) {
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
function getURLFromFilePath(filepath) {
const tmp = new URL('file://');
tmp.pathname = filepath;
return tmp;
}
function NativeURL(ctx) {
this[context] = ctx;
}
@ -1405,6 +1411,7 @@ setURLConstructor(constructUrl);
module.exports = {
toUSVString,
getPathFromURL,
getURLFromFilePath,
URL,
URLSearchParams,
domainToASCII,

58
lib/module.js

@ -24,6 +24,7 @@
const NativeModule = require('native_module');
const util = require('util');
const internalModule = require('internal/module');
const { getURLFromFilePath } = require('internal/url');
const vm = require('vm');
const assert = require('assert').ok;
const fs = require('fs');
@ -32,6 +33,14 @@ const path = require('path');
const internalModuleReadFile = process.binding('fs').internalModuleReadFile;
const internalModuleStat = process.binding('fs').internalModuleStat;
const preserveSymlinks = !!process.binding('config').preserveSymlinks;
const experimentalModules = !!process.binding('config').experimentalModules;
const errors = require('internal/errors');
const Loader = require('internal/loader/Loader');
const ModuleJob = require('internal/loader/ModuleJob');
const { createDynamicModule } = require('internal/loader/ModuleWrap');
const ESMLoader = new Loader();
function stat(filename) {
filename = path._makeLong(filename);
@ -412,7 +421,36 @@ Module._load = function(request, parent, isMain) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain);
var filename = null;
if (isMain) {
let err;
try {
filename = Module._resolveFilename(request, parent, isMain);
} catch (e) {
// try to keep stack
e.stack;
err = e;
}
if (experimentalModules) {
if (filename === null || /\.mjs$/.test(filename)) {
try {
ESMLoader.import(request).catch((e) => {
console.error(e);
process.exit(1);
});
return;
} catch (e) {
// well, it isn't ESM
}
}
}
if (err) {
throw err;
}
} else {
filename = Module._resolveFilename(request, parent, isMain);
}
var cachedModule = Module._cache[filename];
if (cachedModule) {
@ -482,6 +520,19 @@ Module.prototype.load = function(filename) {
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
if (experimentalModules) {
const url = getURLFromFilePath(filename);
if (ESMLoader.moduleMap.has(`${url}`) !== true) {
const ctx = createDynamicModule(['default'], url);
ctx.reflect.exports.default.set(this.exports);
ESMLoader.moduleMap.set(`${url}`,
new ModuleJob(ESMLoader, ctx.module));
} else {
ESMLoader.moduleMap.get(`${url}`).moduleProvider.finish(
Module._cache[filename]);
}
}
};
@ -578,6 +629,11 @@ Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path._makeLong(filename));
};
if (experimentalModules) {
Module._extensions['.mjs'] = function(module, filename) {
throw new errors.Error('ERR_REQUIRE_ESM', filename);
};
}
// bootstrap main module.
Module.runMain = function() {

9
node.gyp

@ -91,6 +91,13 @@
'lib/internal/http.js',
'lib/internal/inspector_async_hook.js',
'lib/internal/linkedlist.js',
'lib/internal/loader/Loader.js',
'lib/internal/loader/ModuleMap.js',
'lib/internal/loader/ModuleJob.js',
'lib/internal/loader/ModuleWrap.js',
'lib/internal/loader/resolveRequestUrl.js',
'lib/internal/loader/search.js',
'lib/internal/safe_globals.js',
'lib/internal/net.js',
'lib/internal/module.js',
'lib/internal/os.js',
@ -177,6 +184,7 @@
'src/fs_event_wrap.cc',
'src/handle_wrap.cc',
'src/js_stream.cc',
'src/module_wrap.cc',
'src/node.cc',
'src/node_api.cc',
'src/node_api.h',
@ -230,6 +238,7 @@
'src/env-inl.h',
'src/handle_wrap.h',
'src/js_stream.h',
'src/module_wrap.h',
'src/node.h',
'src/node_http2_core.h',
'src/node_http2_core-inl.h',

531
src/module_wrap.cc

@ -0,0 +1,531 @@
#include <algorithm>
#include <limits.h> // PATH_MAX
#include <sys/stat.h> // S_IFDIR
#include "module_wrap.h"
#include "env.h"
#include "node_url.h"
#include "util.h"
#include "util-inl.h"
namespace node {
namespace loader {
using node::url::URL;
using node::url::URL_FLAGS_FAILED;
using v8::Context;
using v8::EscapableHandleScope;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Integer;
using v8::IntegrityLevel;
using v8::Isolate;
using v8::JSON;
using v8::Local;
using v8::MaybeLocal;
using v8::Module;
using v8::Object;
using v8::Persistent;
using v8::Promise;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::Value;
static const char* EXTENSIONS[] = {".mjs", ".js", ".json", ".node"};
std::map<int, std::vector<ModuleWrap*>*> ModuleWrap::module_map_;
ModuleWrap::ModuleWrap(Environment* env,
Local<Object> object,
Local<Module> module,
Local<String> url) : BaseObject(env, object) {
Isolate* iso = Isolate::GetCurrent();
module_.Reset(iso, module);
url_.Reset(iso, url);
}
ModuleWrap::~ModuleWrap() {
Local<Module> module = module_.Get(Isolate::GetCurrent());
std::vector<ModuleWrap*>* same_hash = module_map_[module->GetIdentityHash()];
auto it = std::find(same_hash->begin(), same_hash->end(), this);
if (it != same_hash->end()) {
same_hash->erase(it);
}
module_.Reset();
}
void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* iso = args.GetIsolate();
if (!args.IsConstructCall()) {
env->ThrowError("constructor must be called using new");
return;
}
if (args.Length() != 2) {
env->ThrowError("constructor must have exactly 2 arguments "
"(string, string)");
return;
}
if (!args[0]->IsString()) {
env->ThrowError("first argument is not a string");
return;
}
auto source_text = args[0].As<String>();
if (!args[1]->IsString()) {
env->ThrowError("second argument is not a string");
return;
}
Local<String> url = args[1].As<String>();
Local<Module> mod;
// compile
{
ScriptOrigin origin(url,
Integer::New(iso, 0),
Integer::New(iso, 0),
False(iso),
Integer::New(iso, 0),
FIXED_ONE_BYTE_STRING(iso, ""),
False(iso),
False(iso),
True(iso));
ScriptCompiler::Source source(source_text, origin);
auto maybe_mod = ScriptCompiler::CompileModule(iso, &source);
if (maybe_mod.IsEmpty()) {
return;
}
mod = maybe_mod.ToLocalChecked();
}
auto that = args.This();
auto ctx = that->CreationContext();
auto url_str = FIXED_ONE_BYTE_STRING(iso, "url");
if (!that->Set(ctx, url_str, url).FromMaybe(false)) {
return;
}
ModuleWrap* obj =
new ModuleWrap(Environment::GetCurrent(ctx), that, mod, url);
if (ModuleWrap::module_map_.count(mod->GetIdentityHash()) == 0) {
ModuleWrap::module_map_[mod->GetIdentityHash()] =
new std::vector<ModuleWrap*>();
}
ModuleWrap::module_map_[mod->GetIdentityHash()]->push_back(obj);
Wrap(that, obj);
that->SetIntegrityLevel(ctx, IntegrityLevel::kFrozen);
args.GetReturnValue().Set(that);
}
void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* iso = args.GetIsolate();
EscapableHandleScope handle_scope(iso);
if (!args[0]->IsFunction()) {
env->ThrowError("first argument is not a function");
return;
}
Local<Function> resolver_arg = args[0].As<Function>();
auto that = args.This();
ModuleWrap* obj = Unwrap<ModuleWrap>(that);
auto mod_context = that->CreationContext();
if (obj->linked_) return;
obj->linked_ = true;
Local<Module> mod(obj->module_.Get(iso));
// call the dependency resolve callbacks
for (int i = 0; i < mod->GetModuleRequestsLength(); i++) {
Local<String> specifier = mod->GetModuleRequest(i);
Utf8Value specifier_utf(env->isolate(), specifier);
std::string specifier_std(*specifier_utf, specifier_utf.length());
Local<Value> argv[] = {
specifier
};
MaybeLocal<Value> maybe_resolve_return_value =
resolver_arg->Call(mod_context, that, 1, argv);
if (maybe_resolve_return_value.IsEmpty()) {
return;
}
Local<Value> resolve_return_value =
maybe_resolve_return_value.ToLocalChecked();
if (!resolve_return_value->IsPromise()) {
env->ThrowError("linking error, expected resolver to return a promise");
}
Local<Promise> resolve_promise = resolve_return_value.As<Promise>();
obj->resolve_cache_[specifier_std] = new Persistent<Promise>();
obj->resolve_cache_[specifier_std]->Reset(iso, resolve_promise);
}
args.GetReturnValue().Set(handle_scope.Escape(that));
}
void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
auto iso = args.GetIsolate();
auto that = args.This();
auto ctx = that->CreationContext();
ModuleWrap* obj = Unwrap<ModuleWrap>(that);
Local<Module> mod = obj->module_.Get(iso);
bool ok = mod->Instantiate(ctx, ModuleWrap::ResolveCallback);
// clear resolve cache on instantiate
obj->resolve_cache_.clear();
if (!ok) {
return;
}
}
void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
auto iso = args.GetIsolate();
auto that = args.This();
auto ctx = that->CreationContext();
ModuleWrap* obj = Unwrap<ModuleWrap>(that);
auto result = obj->module_.Get(iso)->Evaluate(ctx);
if (result.IsEmpty()) {
return;
}
auto ret = result.ToLocalChecked();
args.GetReturnValue().Set(ret);
}
MaybeLocal<Module> ModuleWrap::ResolveCallback(Local<Context> context,
Local<String> specifier,
Local<Module> referrer) {
Environment* env = Environment::GetCurrent(context);
Isolate* iso = Isolate::GetCurrent();
if (ModuleWrap::module_map_.count(referrer->GetIdentityHash()) == 0) {
env->ThrowError("linking error, unknown module");
return MaybeLocal<Module>();
}
std::vector<ModuleWrap*>* possible_deps =
ModuleWrap::module_map_[referrer->GetIdentityHash()];
ModuleWrap* dependent = nullptr;
for (auto possible_dep : *possible_deps) {
if (possible_dep->module_ == referrer) {
dependent = possible_dep;
}
}
if (dependent == nullptr) {
env->ThrowError("linking error, null dep");
return MaybeLocal<Module>();
}
Utf8Value specifier_utf(env->isolate(), specifier);
std::string specifier_std(*specifier_utf, specifier_utf.length());
if (dependent->resolve_cache_.count(specifier_std) != 1) {
env->ThrowError("linking error, not in local cache");
return MaybeLocal<Module>();
}
Local<Promise> resolve_promise =
dependent->resolve_cache_[specifier_std]->Get(iso);
if (resolve_promise->State() != Promise::kFulfilled) {
env->ThrowError("linking error, dependency promises must be resolved on "
"instantiate");
return MaybeLocal<Module>();
}
auto module_object = resolve_promise->Result().As<Object>();
if (module_object.IsEmpty() || !module_object->IsObject()) {
env->ThrowError("linking error, expected a valid module object from "
"resolver");
return MaybeLocal<Module>();
}
ModuleWrap* mod;
ASSIGN_OR_RETURN_UNWRAP(&mod, module_object, MaybeLocal<Module>());
return mod->module_.Get(env->isolate());
}
namespace {
URL __init_cwd() {
std::string specifier = "file://";
#ifdef _WIN32
// MAX_PATH is in characters, not bytes. Make sure we have enough headroom.
char buf[MAX_PATH * 4];
#else
char buf[PATH_MAX];
#endif
size_t cwd_len = sizeof(buf);
int err = uv_cwd(buf, &cwd_len);
if (err) {
return URL("");
}
specifier += buf;
specifier += "/";
return URL(specifier);
}
static URL INITIAL_CWD(__init_cwd());
inline bool is_relative_or_absolute_path(std::string specifier) {
auto len = specifier.length();
if (len <= 0) {
return false;
} else if (specifier[0] == '/') {
return true;
} else if (specifier[0] == '.') {
if (len == 1 || specifier[1] == '/') {
return true;
} else if (specifier[1] == '.') {
if (len == 2 || specifier[2] == '/') {
return true;
}
}
}
return false;
}
struct read_result {
bool had_error = false;
std::string source;
} read_result;
inline const struct read_result read_file(uv_file file) {
struct read_result ret;
std::string src;
uv_fs_t req;
void* base = malloc(4096);
if (base == nullptr) {
ret.had_error = true;
return ret;
}
uv_buf_t buf = uv_buf_init(static_cast<char*>(base), 4096);
uv_fs_read(uv_default_loop(), &req, file, &buf, 1, 0, nullptr);
while (req.result > 0) {
src += std::string(static_cast<const char*>(buf.base), req.result);
uv_fs_read(uv_default_loop(), &req, file, &buf, 1, src.length(), nullptr);
}
ret.source = src;
return ret;
}
struct file_check {
bool failed = true;
uv_file file;
} file_check;
inline const struct file_check check_file(URL search,
bool close = false,
bool allow_dir = false) {
struct file_check ret;
uv_fs_t fs_req;
std::string path = search.ToFilePath();
if (path.empty()) {
return ret;
}
uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr);
auto fd = fs_req.result;
if (fd < 0) {
return ret;
}
if (!allow_dir) {
uv_fs_fstat(nullptr, &fs_req, fd, nullptr);
if (fs_req.statbuf.st_mode & S_IFDIR) {
uv_fs_close(nullptr, &fs_req, fd, nullptr);
return ret;
}
}
ret.failed = false;
ret.file = fd;
if (close) uv_fs_close(nullptr, &fs_req, fd, nullptr);
return ret;
}
URL resolve_extensions(URL search, bool check_exact = true) {
if (check_exact) {
auto check = check_file(search, true);
if (!check.failed) {
return search;
}
}
for (auto extension : EXTENSIONS) {
URL guess(search.path() + extension, &search);
auto check = check_file(guess, true);
if (!check.failed) {
return guess;
}
}
return URL("");
}
inline URL resolve_index(URL search) {
return resolve_extensions(URL("index", &search), false);
}
URL resolve_main(URL search) {
URL pkg("package.json", &search);
auto check = check_file(pkg);
if (!check.failed) {
auto iso = Isolate::GetCurrent();
auto ctx = iso->GetCurrentContext();
auto read = read_file(check.file);
uv_fs_t fs_req;
// if we fail to close :-/
uv_fs_close(nullptr, &fs_req, check.file, nullptr);
if (read.had_error) return URL("");
std::string pkg_src = read.source;
Local<String> src =
String::NewFromUtf8(iso, pkg_src.c_str(),
String::kNormalString, pkg_src.length());
if (src.IsEmpty()) return URL("");
auto maybe_pkg_json = JSON::Parse(ctx, src);
if (maybe_pkg_json.IsEmpty()) return URL("");
auto pkg_json_obj = maybe_pkg_json.ToLocalChecked().As<Object>();
if (!pkg_json_obj->IsObject()) return URL("");
auto maybe_pkg_main = pkg_json_obj->Get(
ctx, FIXED_ONE_BYTE_STRING(iso, "main"));
if (maybe_pkg_main.IsEmpty()) return URL("");
auto pkg_main_str = maybe_pkg_main.ToLocalChecked().As<String>();
if (!pkg_main_str->IsString()) return URL("");
Utf8Value main_utf8(iso, pkg_main_str);
std::string main_std(*main_utf8, main_utf8.length());
if (!is_relative_or_absolute_path(main_std)) {
main_std.insert(0, "./");
}
return Resolve(main_std, &search);
}
return URL("");
}
URL resolve_module(std::string specifier, URL* base) {
URL parent(".", base);
URL dir("");
do {
dir = parent;
auto check = Resolve("./node_modules/" + specifier, &dir, true);
if (!(check.flags() & URL_FLAGS_FAILED)) {
const auto limit = specifier.find('/');
const auto spec_len = limit == std::string::npos ?
specifier.length() :
limit + 1;
std::string chroot =
dir.path() + "node_modules/" + specifier.substr(0, spec_len);
if (check.path().substr(0, chroot.length()) != chroot) {
return URL("");
}
return check;
} else {
// TODO(bmeck) PREVENT FALLTHROUGH
}
parent = URL("..", &dir);
} while (parent.path() != dir.path());
return URL("");
}
URL resolve_directory(URL search, bool read_pkg_json) {
if (read_pkg_json) {
auto main = resolve_main(search);
if (!(main.flags() & URL_FLAGS_FAILED)) return main;
}
return resolve_index(search);
}
} // anonymous namespace
URL Resolve(std::string specifier, URL* base, bool read_pkg_json) {
URL pure_url(specifier);
if (!(pure_url.flags() & URL_FLAGS_FAILED)) {
return pure_url;
}
if (specifier.length() == 0) {
return URL("");
}
if (is_relative_or_absolute_path(specifier)) {
URL resolved(specifier, base);
auto file = resolve_extensions(resolved);
if (!(file.flags() & URL_FLAGS_FAILED)) return file;
if (specifier.back() != '/') {
resolved = URL(specifier + "/", base);
}
return resolve_directory(resolved, read_pkg_json);
} else {
return resolve_module(specifier, base);
}
return URL("");
}
void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (args.IsConstructCall()) {
env->ThrowError("resolve() must not be called as a constructor");
return;
}
if (args.Length() != 2) {
env->ThrowError("resolve must have exactly 2 arguments (string, string)");
return;
}
if (!args[0]->IsString()) {
env->ThrowError("first argument is not a string");
return;
}
Utf8Value specifier_utf(env->isolate(), args[0]);
if (!args[1]->IsString()) {
env->ThrowError("second argument is not a string");
return;
}
Utf8Value url_utf(env->isolate(), args[1]);
URL url(*url_utf, url_utf.length());
if (url.flags() & URL_FLAGS_FAILED) {
env->ThrowError("second argument is not a URL string");
return;
}
URL result = node::loader::Resolve(*specifier_utf, &url, true);
if (result.flags() & URL_FLAGS_FAILED) {
std::string msg = "module ";
msg += *specifier_utf;
msg += " not found";
env->ThrowError(msg.c_str());
return;
}
args.GetReturnValue().Set(result.ToObject(env));
}
void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
Local<FunctionTemplate> tpl = env->NewFunctionTemplate(New);
tpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"));
tpl->InstanceTemplate()->SetInternalFieldCount(1);
env->SetProtoMethod(tpl, "link", Link);
env->SetProtoMethod(tpl, "instantiate", Instantiate);
env->SetProtoMethod(tpl, "evaluate", Evaluate);
target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction());
env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve);
}
} // namespace loader
} // namespace node
NODE_MODULE_CONTEXT_AWARE_BUILTIN(module_wrap,
node::loader::ModuleWrap::Initialize)

58
src/module_wrap.h

@ -0,0 +1,58 @@
#ifndef SRC_MODULE_WRAP_H_
#define SRC_MODULE_WRAP_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <map>
#include <string>
#include <vector>
#include "node_url.h"
#include "base-object.h"
#include "base-object-inl.h"
namespace node {
namespace loader {
node::url::URL Resolve(std::string specifier, node::url::URL* base,
bool read_pkg_json = false);
class ModuleWrap : public BaseObject {
public:
static const std::string EXTENSIONS[];
static void Initialize(v8::Local<v8::Object> target,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context);
private:
ModuleWrap(node::Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::Module> module,
v8::Local<v8::String> url);
~ModuleWrap();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Link(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Evaluate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetUrl(v8::Local<v8::String> property,
const v8::PropertyCallbackInfo<v8::Value>& info);
static void Resolve(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Module> ResolveCallback(
v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,
v8::Local<v8::Module> referrer);
v8::Persistent<v8::Module> module_;
v8::Persistent<v8::String> url_;
bool linked_ = false;
std::map<std::string, v8::Persistent<v8::Promise>*> resolve_cache_;
static std::map<int, std::vector<ModuleWrap*>*> module_map_;
};
} // namespace loader
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_MODULE_WRAP_H_

8
src/node.cc

@ -225,6 +225,11 @@ bool trace_warnings = false;
// that is used by lib/module.js
bool config_preserve_symlinks = false;
// Set in node.cc by ParseArgs when --experimental-modules is used.
// Used in node_config.cc to set a constant on process.binding('config')
// that is used by lib/module.js
bool config_experimental_modules = false;
// Set by ParseArgs when --pending-deprecation or NODE_PENDING_DEPRECATION
// is used.
bool config_pending_deprecation = false;
@ -3711,6 +3716,7 @@ static void PrintHelp() {
" note: linked-in ICU data is present\n"
#endif
" --preserve-symlinks preserve symbolic links when resolving\n"
" --experimental-modules experimental ES Module support\n"
" and caching modules\n"
#endif
"\n"
@ -3947,6 +3953,8 @@ static void ParseArgs(int* argc,
Revert(cve);
} else if (strcmp(arg, "--preserve-symlinks") == 0) {
config_preserve_symlinks = true;
} else if (strcmp(arg, "--experimental-modules") == 0) {
config_experimental_modules = true;
} else if (strcmp(arg, "--prof-process") == 0) {
prof_process = true;
short_circuit = true;

3
src/node_config.cc

@ -65,6 +65,9 @@ static void InitConfig(Local<Object> target,
if (config_preserve_symlinks)
READONLY_BOOLEAN_PROPERTY("preserveSymlinks");
if (config_experimental_modules)
READONLY_BOOLEAN_PROPERTY("experimentalModules");
if (config_pending_deprecation)
READONLY_BOOLEAN_PROPERTY("pendingDeprecation");

4
src/node_internals.h

@ -86,6 +86,10 @@ extern bool config_preserve_symlinks;
// Set in node.cc by ParseArgs when --expose-http2 is used.
extern bool config_expose_http2;
// Set in node.cc by ParseArgs when --experimental-modules is used.
// Used in node_config.cc to set a constant on process.binding('config')
// that is used by lib/module.js
extern bool config_experimental_modules;
// Set in node.cc by ParseArgs when --expose-internals or --expose_internals is
// used.

63
src/node_url.cc

@ -2080,6 +2080,69 @@ static void DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
v8::NewStringType::kNormal).ToLocalChecked());
}
std::string URL::ToFilePath() {
if (context_.scheme != "file:") {
return "";
}
#ifdef _WIN32
const char* slash = "\\";
auto is_slash = [] (char ch) {
return ch == '/' || ch == '\\';
};
#else
const char* slash = "/";
auto is_slash = [] (char ch) {
return ch == '/';
};
if ((context_.flags & URL_FLAGS_HAS_HOST) &&
context_.host.length() > 0) {
return "";
}
#endif
std::string decoded_path;
for (std::string& part : context_.path) {
std::string decoded;
PercentDecode(part.c_str(), part.length(), &decoded);
for (char& ch : decoded) {
if (is_slash(ch)) {
return "";
}
}
decoded_path += slash + decoded;
}
#ifdef _WIN32
// TODO(TimothyGu): Use "\\?\" long paths on Windows.
// If hostname is set, then we have a UNC path. Pass the hostname through
// ToUnicode just in case it is an IDN using punycode encoding. We do not
// need to worry about percent encoding because the URL parser will have
// already taken care of that for us. Note that this only causes IDNs with an
// appropriate `xn--` prefix to be decoded.
if ((context_.flags & URL_FLAGS_HAS_HOST) &&
context_.host.length() > 0) {
std::string unicode_host;
if (!ToUnicode(&context_.host, &unicode_host)) {
return "";
}
return "\\\\" + unicode_host + decoded_path;
}
// Otherwise, it's a local path that requires a drive letter.
if (decoded_path.length() < 3) {
return "";
}
if (decoded_path[2] != ':' ||
!IsASCIIAlpha(decoded_path[1])) {
return "";
}
// Strip out the leading '\'.
return decoded_path.substr(1);
#else
return decoded_path;
#endif
}
// This function works by calling out to a JS function that creates and
// returns the JS URL object. Be mindful of the JS<->Native boundary
// crossing that is required.

4
src/node_url.h

@ -163,6 +163,10 @@ class URL {
return ret;
}
// Get the path of the file: URL in a format consumable by native file system
// APIs. Returns an empty string if something went wrong.
std::string ToFilePath();
const Local<Value> ToObject(Environment* env) const;
private:

25
test/cctest/test_url.cc

@ -79,3 +79,28 @@ TEST_F(URLTest, Base3) {
EXPECT_EQ(simple.host(), "example.org");
EXPECT_EQ(simple.path(), "/baz");
}
TEST_F(URLTest, ToFilePath) {
#define T(url, path) EXPECT_EQ(path, URL(url).ToFilePath())
T("http://example.org/foo/bar", "");
#ifdef _WIN32
T("file:///C:/Program%20Files/", "C:\\Program Files\\");
T("file:///C:/a/b/c?query#fragment", "C:\\a\\b\\c");
T("file://host/path/a/b/c?query#fragment", "\\\\host\\path\\a\\b\\c");
T("file://xn--weird-prdj8vva.com/host/a", "\\\\wͪ͊eiͬ͋rd.com\\host\\a");
T("file:///C:/a%2Fb", "");
T("file:///", "");
T("file:///home", "");
#else
T("file:///", "/");
T("file:///home/user?query#fragment", "/home/user");
T("file:///home/user/?query#fragment", "/home/user/");
T("file:///home/user/%20space", "/home/user/ space");
T("file:///home/us%5Cer", "/home/us\\er");
T("file:///home/us%2Fer", "");
T("file://host/path", "");
#endif
#undef T
}

7
test/es-module/es-module.status

@ -0,0 +1,7 @@
prefix parallel
# To mark a test as flaky, list the test name in the appropriate section
# below, without ".js", followed by ": PASS,FLAKY". Example:
# sample-test : PASS,FLAKY
[true] # This section applies to all platforms

5
test/es-module/esm-snapshot-mutator.js

@ -0,0 +1,5 @@
/* eslint-disable required-modules */
'use strict';
const shouldSnapshotFilePath = require.resolve('./esm-snapshot.js');
require('./esm-snapshot.js');
require.cache[shouldSnapshotFilePath].exports++;

3
test/es-module/esm-snapshot.js

@ -0,0 +1,3 @@
/* eslint-disable required-modules */
'use strict';
module.exports = 1;

8
test/es-module/test-esm-basic-imports.mjs

@ -0,0 +1,8 @@
// Flags: --experimental-modules
import '../common';
import assert from 'assert';
import ok from './test-esm-ok.mjs';
import okShebang from './test-esm-shebang.mjs';
assert(ok);
assert(okShebang);

10
test/es-module/test-esm-encoded-path-native.js

@ -0,0 +1,10 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { spawn } = require('child_process');
const native = `${common.fixturesDir}/es-module-url/native.mjs`;
const child = spawn(process.execPath, ['--experimental-modules', native]);
child.on('exit', (code) => {
assert.strictEqual(code, 1);
});

7
test/es-module/test-esm-encoded-path.mjs

@ -0,0 +1,7 @@
// Flags: --experimental-modules
import '../common';
import assert from 'assert';
// ./test-esm-ok.mjs
import ok from './test-%65%73%6d-ok.mjs';
assert(ok);

24
test/es-module/test-esm-forbidden-globals.mjs

@ -0,0 +1,24 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
if (typeof arguments !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof this !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof exports !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof require !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof module !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof __filename !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof __dirname !== 'undefined') {
throw new Error('not an ESM');
}

7
test/es-module/test-esm-namespace.mjs

@ -0,0 +1,7 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import * as fs from 'fs';
import assert from 'assert';
assert.deepStrictEqual(Object.keys(fs), ['default']);

5
test/es-module/test-esm-ok.mjs

@ -0,0 +1,5 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
const isJs = true;
export default isJs;

8
test/es-module/test-esm-pkg-over-ext.mjs

@ -0,0 +1,8 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import resolved from '../fixtures/module-pkg-over-ext/inner';
import expected from '../fixtures/module-pkg-over-ext/inner/package.json';
import assert from 'assert';
assert.strictEqual(resolved, expected);

38
test/es-module/test-esm-preserve-symlinks.js

@ -0,0 +1,38 @@
// Flags: --experimental-modules
'use strict';
const common = require('../common');
const { spawn } = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
common.refreshTmpDir();
const tmpDir = common.tmpDir;
const entry = path.join(tmpDir, 'entry.js');
const real = path.join(tmpDir, 'real.js');
const link_absolute_path = path.join(tmpDir, 'link.js');
fs.writeFileSync(entry, `
const assert = require('assert');
global.x = 0;
require('./real.js');
assert.strictEqual(x, 1);
require('./link.js');
assert.strictEqual(x, 2);
`);
fs.writeFileSync(real, 'x++;');
try {
fs.symlinkSync(real, link_absolute_path);
} catch (err) {
if (err.code !== 'EPERM') throw err;
common.skip('insufficient privileges for symlinks');
}
spawn(process.execPath,
['--experimental-modules', '--preserve-symlinks', entry],
{ stdio: 'inherit' }).on('exit', (code) => {
assert.strictEqual(code, 0);
});

7
test/es-module/test-esm-require-cache.mjs

@ -0,0 +1,7 @@
// Flags: --experimental-modules
import '../common';
import '../fixtures/es-module-require-cache/preload.js';
import '../fixtures/es-module-require-cache/counter.js';
import assert from 'assert';
assert.strictEqual(global.counter, 1);
delete global.counter;

6
test/es-module/test-esm-shebang.mjs

@ -0,0 +1,6 @@
#! }]) // isn't js
// Flags: --experimental-modules
/* eslint-disable required-modules */
const isJs = true;
export default isJs;

7
test/es-module/test-esm-snapshot.mjs

@ -0,0 +1,7 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import './esm-snapshot-mutator';
import one from './esm-snapshot';
import assert from 'assert';
assert.strictEqual(one, 1);

48
test/es-module/test-esm-symlink.js

@ -0,0 +1,48 @@
'use strict';
const common = require('../common');
const { spawn } = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
common.refreshTmpDir();
const tmpDir = common.tmpDir;
const entry = path.join(tmpDir, 'entry.mjs');
const real = path.join(tmpDir, 'index.mjs');
const link_absolute_path = path.join(tmpDir, 'absolute');
const link_relative_path = path.join(tmpDir, 'relative');
const link_ignore_extension = path.join(tmpDir,
'ignore_extension.json');
const link_directory = path.join(tmpDir, 'directory');
fs.writeFileSync(real, 'export default [];');
fs.writeFileSync(entry, `
import assert from 'assert';
import real from './index.mjs';
import absolute from './absolute';
import relative from './relative';
import ignoreExtension from './ignore_extension.json';
import directory from './directory';
assert.strictEqual(absolute, real);
assert.strictEqual(relative, real);
assert.strictEqual(ignoreExtension, real);
assert.strictEqual(directory, real);
`);
try {
fs.symlinkSync(real, link_absolute_path);
fs.symlinkSync(path.basename(real), link_relative_path);
fs.symlinkSync(real, link_ignore_extension);
fs.symlinkSync(path.dirname(real), link_directory);
} catch (err) {
if (err.code !== 'EPERM') throw err;
common.skip('insufficient privileges for symlinks');
}
spawn(process.execPath, ['--experimental-modules', entry],
{ stdio: 'inherit' }).on('exit', (code) => {
assert.strictEqual(code, 0);
});

6
test/es-module/testcfg.py

@ -0,0 +1,6 @@
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import testpy
def GetConfiguration(context, root):
return testpy.SimpleTestConfiguration(context, root, 'es-module')

2
test/fixtures/es-module-require-cache/counter.js

@ -0,0 +1,2 @@
global.counter = global.counter || 0;
global.counter++;

1
test/fixtures/es-module-require-cache/preload.js

@ -0,0 +1 @@
require('./counter');

0
test/fixtures/es-module-url/empty.js

2
test/fixtures/es-module-url/native.mjs

@ -0,0 +1,2 @@
// path
import 'p%61th';

13
test/testpy/__init__.py

@ -27,7 +27,7 @@
import test
import os
from os.path import join, dirname, exists
from os.path import join, dirname, exists, splitext
import re
import ast
@ -109,18 +109,17 @@ class SimpleTestConfiguration(test.TestConfiguration):
self.additional_flags = []
def Ls(self, path):
def SelectTest(name):
return name.startswith('test-') and name.endswith('.js')
return [f[:-3] for f in os.listdir(path) if SelectTest(f)]
return [f for f in os.listdir(path) if re.match('^test-.*\.m?js$', f)]
def ListTests(self, current_path, path, arch, mode):
all_tests = [current_path + [t] for t in self.Ls(join(self.root))]
result = []
for test in all_tests:
if self.Contains(path, test):
file_path = join(self.root, reduce(join, test[1:], "") + ".js")
result.append(SimpleTestCase(test, file_path, arch, mode, self.context,
self, self.additional_flags))
file_path = join(self.root, reduce(join, test[1:], ""))
test_name = test[:-1] + [splitext(test[-1])[0]]
result.append(SimpleTestCase(test_name, file_path, arch, mode,
self.context, self, self.additional_flags))
return result
def GetBuildRequirements(self):

60
tools/eslint-rules/required-modules.js

@ -13,6 +13,7 @@ const path = require('path');
module.exports = function(context) {
// trim required module names
var requiredModules = context.options;
const isESM = context.parserOptions.sourceType === 'module';
const foundModules = [];
@ -39,39 +40,35 @@ module.exports = function(context) {
return node.callee.type === 'Identifier' && node.callee.name === 'require';
}
/**
* Function to check if the path is a required module and return its name.
* @param {String} str The path to check
* @returns {undefined|String} required module name or undefined
*/
function getRequiredModuleName(str) {
var value = path.basename(str);
// check if value is in required modules array
return requiredModules.indexOf(value) !== -1 ? value : undefined;
}
/**
* Function to check if a node has an argument that is a required module and
* return its name.
* @param {ASTNode} node The node to check
* @returns {undefined|String} required module name or undefined
*/
function getRequiredModuleName(node) {
var moduleName;
function getRequiredModuleNameFromCall(node) {
// node has arguments and first argument is string
if (node.arguments.length && isString(node.arguments[0])) {
var argValue = path.basename(node.arguments[0].value.trim());
// check if value is in required modules array
if (requiredModules.indexOf(argValue) !== -1) {
moduleName = argValue;
}
return getRequiredModuleName(node.arguments[0].value.trim());
}
return moduleName;
return undefined;
}
return {
'CallExpression': function(node) {
if (isRequireCall(node)) {
var requiredModuleName = getRequiredModuleName(node);
if (requiredModuleName) {
foundModules.push(requiredModuleName);
}
}
},
'Program:exit': function(node) {
const rules = {
'Program:exit'(node) {
if (foundModules.length < requiredModules.length) {
var missingModules = requiredModules.filter(
function(module) {
@ -88,6 +85,27 @@ module.exports = function(context) {
}
}
};
if (isESM) {
rules.ImportDeclaration = (node) => {
var requiredModuleName = getRequiredModuleName(node.source.value);
if (requiredModuleName) {
foundModules.push(requiredModuleName);
}
};
} else {
rules.CallExpression = (node) => {
if (isRequireCall(node)) {
var requiredModuleName = getRequiredModuleNameFromCall(node);
if (requiredModuleName) {
foundModules.push(requiredModuleName);
}
}
};
}
return rules;
};
module.exports.schema = {

18
tools/test.py

@ -279,9 +279,7 @@ class TapProgressIndicator(SimpleProgressIndicator):
# hard to decipher what test is running when only the filename is printed.
prefix = abspath(join(dirname(__file__), '../test')) + os.sep
command = output.command[-1]
if command.endswith('.js'): command = command[:-3]
if command.startswith(prefix): command = command[len(prefix):]
command = command.replace('\\', '/')
command = NormalizePath(command, prefix)
if output.UnexpectedOutput():
status_line = 'not ok %i %s' % (self._done, command)
@ -352,9 +350,7 @@ class DeoptsCheckProgressIndicator(SimpleProgressIndicator):
# hard to decipher what test is running when only the filename is printed.
prefix = abspath(join(dirname(__file__), '../test')) + os.sep
command = output.command[-1]
if command.endswith('.js'): command = command[:-3]
if command.startswith(prefix): command = command[len(prefix):]
command = command.replace('\\', '/')
command = NormalizePath(command, prefix)
stdout = output.output.stdout.strip()
printed_file = False
@ -1509,12 +1505,16 @@ def SplitPath(s):
stripped = [ c.strip() for c in s.split('/') ]
return [ Pattern(s) for s in stripped if len(s) > 0 ]
def NormalizePath(path):
def NormalizePath(path, prefix='test/'):
# strip the extra path information of the specified test
if path.startswith('test/'):
path = path[5:]
prefix = prefix.replace('\\', '/')
path = path.replace('\\', '/')
if path.startswith(prefix):
path = path[len(prefix):]
if path.endswith('.js'):
path = path[:-3]
elif path.endswith('.mjs'):
path = path[:-4]
return path
def GetSpecialCommandProcessor(value):

Loading…
Cancel
Save