Skip to content

Commit 1942cc8

Browse files
committed
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. Author: Bradley Farias <[email protected]> Author: Guy Bedford <[email protected]> Author: Jan Krems <[email protected]> fix eslint errors add missing include restore module.exports remove TryCatch Use anonymous namespace, fix compilation warnings Clean up string usage Fix c++ linting Allow unlinked cwd Fixes make test. fix indentation eslint fix more common whitespace usage, fix missing header include try to address Windows build failure throw no-base error in JS No need to go to C++ for this. Fix resolve module dir URL Fix module.load usage Makes CJS module loading work process.exit(1) when there is an error in ESM loading Add support for .mjs in test harness tools: support ESM in ESLint required-modules rule Start porting ESM tests Disable required-modules for basic mjs tests Avoid using auto for easy-to-mix up cases fix linter errors for new linter rules on `master` More linting use null as [[Prototype]] directly move src/loader/* out of its own directory fix more linting properly decode url encoding in pathnames Cleanup URL->file path conversion Error cases, add tests. lint fix module._cache regression move test out of fixtures nits on test properly realpath after getting path from URL reject module instantiation when binding throws call e.stack getter preserve-symlinks support remove unnecessary index file flag as harmony-modules lint ordering of lists add .mjs to coverage, ESM support appears buggy though simple ESM namespace keys test move logic for making URL from file path f test and fix snapshot timing of CJS use internal/errors fix bug with moduleProvider in CJS snapshot nits from @Fishrock123 linting gate
1 parent 365c245 commit 1942cc8

43 files changed

Lines changed: 1481 additions & 40 deletions

Some content is hidden

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

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ coverage-build: all
150150
"$(CURDIR)/testing/coverage/gcovr-patches.diff"); fi
151151
if [ -d lib_ ]; then $(RM) -r lib; mv lib_ lib; fi
152152
mv lib lib_
153-
$(NODE) ./node_modules/.bin/nyc instrument lib_/ lib/
153+
$(NODE) ./node_modules/.bin/nyc instrument --extension .js --extension .mjs lib_/ lib/
154154
$(MAKE)
155155

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

892894
jslint-ci:
893895
@echo "Running JS linter..."

lib/internal/errors.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ E('ERR_INVALID_OPT_VALUE',
209209
});
210210
E('ERR_INVALID_OPT_VALUE_ENCODING',
211211
(value) => `The value "${String(value)}" is invalid for option "encoding"`);
212+
E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set');
212213
E('ERR_INVALID_PROTOCOL', (protocol, expectedProtocol) =>
213214
`Protocol "${protocol}" not supported. Expected "${expectedProtocol}"`);
214215
E('ERR_INVALID_REPL_EVAL_CONFIG',
@@ -226,14 +227,17 @@ E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe');
226227
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks');
227228
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented');
228229
E('ERR_MISSING_ARGS', missingArgs);
230+
E('ERR_MISSING_MODULE', 'Cannot find module %s');
231+
E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' +
232+
'Legacy behavior in require would have found it at %s');
229233
E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times');
230234
E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function');
231235
E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object');
232236
E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support');
233237
E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
234238
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported');
235239
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
236-
E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set');
240+
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
237241
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');
238242
E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
239243
E('ERR_SOCKET_BAD_TYPE',
@@ -270,6 +274,7 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
270274
'At least one valid performance entry type is required');
271275
E('ERR_VALUE_OUT_OF_RANGE', 'The value of "%s" must be %s. Received "%s"');
272276

277+
273278
function invalidArgType(name, expected, actual) {
274279
assert(name, 'name is required');
275280

lib/internal/loader/Loader.js

Lines changed: 75 additions & 0 deletions
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

Lines changed: 116 additions & 0 deletions
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

Lines changed: 33 additions & 0 deletions
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;

lib/internal/loader/ModuleWrap.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
3+
const { ModuleWrap } = process.binding('module_wrap');
4+
const debug = require('util').debuglog('esm');
5+
const ArrayJoin = Function.call.bind(Array.prototype.join);
6+
const ArrayMap = Function.call.bind(Array.prototype.map);
7+
8+
const getNamespaceOfModuleWrap = (m) => {
9+
const tmp = new ModuleWrap('import * as _ from "";_;', '');
10+
tmp.link(async () => m);
11+
tmp.instantiate();
12+
return tmp.evaluate();
13+
};
14+
15+
const createDynamicModule = (exports, url = '', evaluate) => {
16+
debug(
17+
`creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}`
18+
);
19+
const names = ArrayMap(exports, (name) => `${name}`);
20+
// sanitized ESM for reflection purposes
21+
const src = `export let executor;
22+
${ArrayJoin(ArrayMap(names, (name) => `export let $${name}`), ';\n')}
23+
;(() => [
24+
fn => executor = fn,
25+
{ exports: { ${
26+
ArrayJoin(ArrayMap(names, (name) => `${name}: {
27+
get: () => $${name},
28+
set: v => $${name} = v
29+
}`), ',\n')
30+
} } }
31+
]);
32+
`;
33+
const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`);
34+
reflectiveModule.instantiate();
35+
const [setExecutor, reflect] = reflectiveModule.evaluate()();
36+
// public exposed ESM
37+
const reexports = `import { executor,
38+
${ArrayMap(names, (name) => `$${name}`)}
39+
} from "";
40+
export {
41+
${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')}
42+
}
43+
// add await to this later if top level await comes along
44+
typeof executor === "function" ? executor() : void 0;`;
45+
if (typeof evaluate === 'function') {
46+
setExecutor(() => evaluate(reflect));
47+
}
48+
const runner = new ModuleWrap(reexports, `${url}`);
49+
runner.link(async () => reflectiveModule);
50+
runner.instantiate();
51+
return {
52+
module: runner,
53+
reflect
54+
};
55+
};
56+
57+
module.exports = {
58+
createDynamicModule,
59+
getNamespaceOfModuleWrap,
60+
ModuleWrap
61+
};

0 commit comments

Comments
 (0)