Skip to content

Commit afc87c2

Browse files
devsnekMylesBorins
authored andcommittedFeb 20, 2018
module: refactor loader
PR-URL: #16874 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Bradley Farias <[email protected]>
1 parent 35471bc commit afc87c2

14 files changed

+271
-275
lines changed
 

‎lib/internal/loader/ModuleWrap.js ‎lib/internal/loader/CreateDynamicModule.js

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
'use strict';
22

3-
const {
4-
ModuleWrap,
5-
setImportModuleDynamicallyCallback
6-
} = internalBinding('module_wrap');
3+
const { ModuleWrap } = internalBinding('module_wrap');
74
const debug = require('util').debuglog('esm');
85
const ArrayJoin = Function.call.bind(Array.prototype.join);
96
const ArrayMap = Function.call.bind(Array.prototype.map);
@@ -60,8 +57,4 @@ const createDynamicModule = (exports, url = '', evaluate) => {
6057
};
6158
};
6259

63-
module.exports = {
64-
createDynamicModule,
65-
setImportModuleDynamicallyCallback,
66-
ModuleWrap
67-
};
60+
module.exports = createDynamicModule;

‎lib/internal/loader/DefaultResolve.js

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
const { URL } = require('url');
4+
const CJSmodule = require('module');
5+
const internalURLModule = require('internal/url');
6+
const internalFS = require('internal/fs');
7+
const NativeModule = require('native_module');
8+
const { extname } = require('path');
9+
const { realpathSync } = require('fs');
10+
const preserveSymlinks = !!process.binding('config').preserveSymlinks;
11+
const errors = require('internal/errors');
12+
const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
13+
const StringStartsWith = Function.call.bind(String.prototype.startsWith);
14+
15+
const realpathCache = new Map();
16+
17+
function search(target, base) {
18+
if (base === undefined) {
19+
// We cannot search without a base.
20+
throw new errors.Error('ERR_MISSING_MODULE', target);
21+
}
22+
try {
23+
return moduleWrapResolve(target, base);
24+
} catch (e) {
25+
e.stack; // cause V8 to generate stack before rethrow
26+
let error = e;
27+
try {
28+
const questionedBase = new URL(base);
29+
const tmpMod = new CJSmodule(questionedBase.pathname, null);
30+
tmpMod.paths = CJSmodule._nodeModulePaths(
31+
new URL('./', questionedBase).pathname);
32+
const found = CJSmodule._resolveFilename(target, tmpMod);
33+
error = new errors.Error('ERR_MODULE_RESOLUTION_LEGACY', target,
34+
base, found);
35+
} catch (problemChecking) {
36+
// ignore
37+
}
38+
throw error;
39+
}
40+
}
41+
42+
const extensionFormatMap = {
43+
__proto__: null,
44+
'.mjs': 'esm',
45+
'.json': 'json',
46+
'.node': 'addon',
47+
'.js': 'commonjs'
48+
};
49+
50+
function resolve(specifier, parentURL) {
51+
if (NativeModule.nonInternalExists(specifier)) {
52+
return {
53+
url: specifier,
54+
format: 'builtin'
55+
};
56+
}
57+
58+
let url;
59+
try {
60+
url = search(specifier, parentURL);
61+
} catch (e) {
62+
if (typeof e.message === 'string' &&
63+
StringStartsWith(e.message, 'Cannot find module'))
64+
e.code = 'MODULE_NOT_FOUND';
65+
throw e;
66+
}
67+
68+
if (!preserveSymlinks) {
69+
const real = realpathSync(internalURLModule.getPathFromURL(url), {
70+
[internalFS.realpathCacheKey]: realpathCache
71+
});
72+
const old = url;
73+
url = internalURLModule.getURLFromFilePath(real);
74+
url.search = old.search;
75+
url.hash = old.hash;
76+
}
77+
78+
const ext = extname(url.pathname);
79+
return { url: `${url}`, format: extensionFormatMap[ext] || ext };
80+
}
81+
82+
module.exports = resolve;
83+
// exported for tests
84+
module.exports.search = search;

‎lib/internal/loader/Loader.js

+71-75
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
const path = require('path');
44
const { getURLFromFilePath, URL } = require('internal/url');
5-
6-
const {
7-
createDynamicModule,
8-
setImportModuleDynamicallyCallback
9-
} = require('internal/loader/ModuleWrap');
5+
const errors = require('internal/errors');
106

117
const ModuleMap = require('internal/loader/ModuleMap');
128
const ModuleJob = require('internal/loader/ModuleJob');
13-
const ModuleRequest = require('internal/loader/ModuleRequest');
14-
const errors = require('internal/errors');
9+
const defaultResolve = require('internal/loader/DefaultResolve');
10+
const createDynamicModule = require('internal/loader/CreateDynamicModule');
11+
const translators = require('internal/loader/Translators');
12+
const { setImportModuleDynamicallyCallback } = internalBinding('module_wrap');
13+
const FunctionBind = Function.call.bind(Function.prototype.bind);
14+
1515
const debug = require('util').debuglog('esm');
1616

1717
// Returns a file URL for the current working directory.
@@ -40,105 +40,101 @@ function normalizeReferrerURL(referrer) {
4040
* the main module and everything in its dependency graph. */
4141
class Loader {
4242
constructor(base = getURLStringForCwd()) {
43-
if (typeof base !== 'string') {
43+
if (typeof base !== 'string')
4444
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
45-
}
4645

47-
this.moduleMap = new ModuleMap();
4846
this.base = base;
47+
48+
// methods which translate input code or other information
49+
// into es modules
50+
this.translators = translators;
51+
52+
// registry of loaded modules, akin to `require.cache`
53+
this.moduleMap = new ModuleMap();
54+
4955
// The resolver has the signature
5056
// (specifier : string, parentURL : string, defaultResolve)
51-
// -> Promise<{ url : string,
52-
// format: anything in Loader.validFormats }>
57+
// -> Promise<{ url : string, format: string }>
5358
// where defaultResolve is ModuleRequest.resolve (having the same
5459
// signature itself).
5560
// If `.format` on the returned value is 'dynamic', .dynamicInstantiate
5661
// will be used as described below.
57-
this.resolver = ModuleRequest.resolve;
58-
// This hook is only called when resolve(...).format is 'dynamic' and has
59-
// the signature
62+
this._resolve = defaultResolve;
63+
// This hook is only called when resolve(...).format is 'dynamic' and
64+
// has the signature
6065
// (url : string) -> Promise<{ exports: { ... }, execute: function }>
6166
// Where `exports` is an object whose property names define the exported
6267
// names of the generated module. `execute` is a function that receives
6368
// an object with the same keys as `exports`, whose values are get/set
6469
// functions for the actual exported values.
65-
this.dynamicInstantiate = undefined;
66-
}
67-
68-
hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) {
69-
// Use .bind() to avoid giving access to the Loader instance when it is
70-
// called as this.resolver(...);
71-
this.resolver = resolve.bind(null);
72-
this.dynamicInstantiate = dynamicInstantiate;
70+
this._dynamicInstantiate = undefined;
7371
}
7472

75-
// Typechecking wrapper around .resolver().
7673
async resolve(specifier, parentURL = this.base) {
77-
if (typeof parentURL !== 'string') {
78-
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
79-
'parentURL', 'string');
80-
}
81-
82-
const { url, format } = await this.resolver(specifier, parentURL,
83-
ModuleRequest.resolve);
74+
if (typeof parentURL !== 'string')
75+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURL', 'string');
8476

85-
if (!Loader.validFormats.includes(format)) {
86-
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format',
87-
Loader.validFormats);
88-
}
77+
const { url, format } =
78+
await this._resolve(specifier, parentURL, defaultResolve);
8979

90-
if (typeof url !== 'string') {
80+
if (typeof url !== 'string')
9181
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
92-
}
9382

94-
if (format === 'builtin') {
83+
if (typeof format !== 'string')
84+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format', 'string');
85+
86+
if (format === 'builtin')
9587
return { url: `node:${url}`, format };
96-
}
9788

98-
if (format !== 'dynamic') {
99-
if (!ModuleRequest.loaders.has(format)) {
100-
throw new errors.Error('ERR_UNKNOWN_MODULE_FORMAT', format);
101-
}
102-
if (!url.startsWith('file:')) {
103-
throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:');
104-
}
105-
}
89+
if (format !== 'dynamic' && !url.startsWith('file:'))
90+
throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:');
10691

10792
return { url, format };
10893
}
10994

110-
// May create a new ModuleJob instance if one did not already exist.
95+
async import(specifier, parent = this.base) {
96+
const job = await this.getModuleJob(specifier, parent);
97+
const module = await job.run();
98+
return module.namespace();
99+
}
100+
101+
hook({ resolve, dynamicInstantiate }) {
102+
// Use .bind() to avoid giving access to the Loader instance when called.
103+
if (resolve !== undefined)
104+
this._resolve = FunctionBind(resolve, null);
105+
if (dynamicInstantiate !== undefined)
106+
this._dynamicInstantiate = FunctionBind(dynamicInstantiate, null);
107+
}
108+
111109
async getModuleJob(specifier, parentURL = this.base) {
112110
const { url, format } = await this.resolve(specifier, parentURL);
113111
let job = this.moduleMap.get(url);
114-
if (job === undefined) {
115-
let loaderInstance;
116-
if (format === 'dynamic') {
117-
const { dynamicInstantiate } = this;
118-
if (typeof dynamicInstantiate !== 'function') {
119-
throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK');
120-
}
121-
122-
loaderInstance = async (url) => {
123-
const { exports, execute } = await dynamicInstantiate(url);
124-
return createDynamicModule(exports, url, (reflect) => {
125-
debug(`Loading custom loader ${url}`);
126-
execute(reflect.exports);
127-
});
128-
};
129-
} else {
130-
loaderInstance = ModuleRequest.loaders.get(format);
131-
}
132-
job = new ModuleJob(this, url, loaderInstance);
133-
this.moduleMap.set(url, job);
112+
if (job !== undefined)
113+
return job;
114+
115+
let loaderInstance;
116+
if (format === 'dynamic') {
117+
if (typeof this._dynamicInstantiate !== 'function')
118+
throw new errors.Error('ERR_MISSING_DYNAMIC_INTSTANTIATE_HOOK');
119+
120+
loaderInstance = async (url) => {
121+
debug(`Translating dynamic ${url}`);
122+
const { exports, execute } = await this._dynamicInstantiate(url);
123+
return createDynamicModule(exports, url, (reflect) => {
124+
debug(`Loading dynamic ${url}`);
125+
execute(reflect.exports);
126+
});
127+
};
128+
} else {
129+
if (!translators.has(format))
130+
throw new errors.RangeError('ERR_UNKNOWN_MODULE_FORMAT', format);
131+
132+
loaderInstance = translators.get(format);
134133
}
135-
return job;
136-
}
137134

138-
async import(specifier, parentURL = this.base) {
139-
const job = await this.getModuleJob(specifier, parentURL);
140-
const module = await job.run();
141-
return module.namespace();
135+
job = new ModuleJob(this, url, loaderInstance);
136+
this.moduleMap.set(url, job);
137+
return job;
142138
}
143139

144140
static registerImportDynamicallyCallback(loader) {
@@ -147,6 +143,6 @@ class Loader {
147143
});
148144
}
149145
}
150-
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];
146+
151147
Object.setPrototypeOf(Loader.prototype, null);
152148
module.exports = Loader;

‎lib/internal/loader/ModuleRequest.js

-138
This file was deleted.

‎lib/internal/loader/Translators.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict';
2+
3+
const { ModuleWrap } = internalBinding('module_wrap');
4+
const NativeModule = require('native_module');
5+
const internalCJSModule = require('internal/module');
6+
const CJSModule = require('module');
7+
const internalURLModule = require('internal/url');
8+
const createDynamicModule = require('internal/loader/CreateDynamicModule');
9+
const fs = require('fs');
10+
const { _makeLong } = require('path');
11+
const { SafeMap } = require('internal/safe_globals');
12+
const { URL } = require('url');
13+
const debug = require('util').debuglog('esm');
14+
const readFileAsync = require('util').promisify(fs.readFile);
15+
const readFileSync = fs.readFileSync;
16+
const StringReplace = Function.call.bind(String.prototype.replace);
17+
const JsonParse = JSON.parse;
18+
19+
const translators = new SafeMap();
20+
module.exports = translators;
21+
22+
// Stragety for loading a standard JavaScript module
23+
translators.set('esm', async (url) => {
24+
const source = `${await readFileAsync(new URL(url))}`;
25+
debug(`Translating StandardModule ${url}`);
26+
return {
27+
module: new ModuleWrap(internalCJSModule.stripShebang(source), url),
28+
reflect: undefined
29+
};
30+
});
31+
32+
// Strategy for loading a node-style CommonJS module
33+
const isWindows = process.platform === 'win32';
34+
const winSepRegEx = /\//g;
35+
translators.set('commonjs', async (url) => {
36+
debug(`Translating CJSModule ${url}`);
37+
const pathname = internalURLModule.getPathFromURL(new URL(url));
38+
const module = CJSModule._cache[
39+
isWindows ? StringReplace(pathname, winSepRegEx, '\\') : pathname];
40+
if (module && module.loaded) {
41+
const ctx = createDynamicModule(['default'], url);
42+
ctx.reflect.exports.default.set(module.exports);
43+
return ctx;
44+
}
45+
return createDynamicModule(['default'], url, () => {
46+
debug(`Loading CJSModule ${url}`);
47+
// we don't care about the return val of _load here because Module#load
48+
// will handle it for us by checking the loader registry and filling the
49+
// exports like above
50+
CJSModule._load(pathname);
51+
});
52+
});
53+
54+
// Strategy for loading a node builtin CommonJS module that isn't
55+
// through normal resolution
56+
translators.set('builtin', async (url) => {
57+
debug(`Translating BuiltinModule ${url}`);
58+
return createDynamicModule(['default'], url, (reflect) => {
59+
debug(`Loading BuiltinModule ${url}`);
60+
const exports = NativeModule.require(url.slice(5));
61+
reflect.exports.default.set(exports);
62+
});
63+
});
64+
65+
// Stragety for loading a node native module
66+
translators.set('addon', async (url) => {
67+
debug(`Translating NativeModule ${url}`);
68+
return createDynamicModule(['default'], url, (reflect) => {
69+
debug(`Loading NativeModule ${url}`);
70+
const module = { exports: {} };
71+
const pathname = internalURLModule.getPathFromURL(new URL(url));
72+
process.dlopen(module, _makeLong(pathname));
73+
reflect.exports.default.set(module.exports);
74+
});
75+
});
76+
77+
// Stragety for loading a JSON file
78+
translators.set('json', async (url) => {
79+
debug(`Translating JSONModule ${url}`);
80+
return createDynamicModule(['default'], url, (reflect) => {
81+
debug(`Loading JSONModule ${url}`);
82+
const pathname = internalURLModule.getPathFromURL(new URL(url));
83+
const content = readFileSync(pathname, 'utf8');
84+
try {
85+
const exports = JsonParse(internalCJSModule.stripBOM(content));
86+
reflect.exports.default.set(exports);
87+
} catch (err) {
88+
err.message = pathname + ': ' + err.message;
89+
throw err;
90+
}
91+
});
92+
});

‎lib/internal/loader/search.js

-31
This file was deleted.

‎lib/module.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module.exports = Module;
4545
// these are below module.exports for the circular reference
4646
const Loader = require('internal/loader/Loader');
4747
const ModuleJob = require('internal/loader/ModuleJob');
48-
const { createDynamicModule } = require('internal/loader/ModuleWrap');
48+
const createDynamicModule = require('internal/loader/CreateDynamicModule');
4949
let ESMLoader;
5050

5151
function stat(filename) {

‎node.gyp

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@
104104
'lib/internal/inspector_async_hook.js',
105105
'lib/internal/linkedlist.js',
106106
'lib/internal/loader/Loader.js',
107-
'lib/internal/loader/ModuleMap.js',
107+
'lib/internal/loader/CreateDynamicModule.js',
108+
'lib/internal/loader/DefaultResolve.js',
108109
'lib/internal/loader/ModuleJob.js',
109-
'lib/internal/loader/ModuleWrap.js',
110-
'lib/internal/loader/ModuleRequest.js',
111-
'lib/internal/loader/search.js',
110+
'lib/internal/loader/ModuleMap.js',
111+
'lib/internal/loader/Translators.js',
112112
'lib/internal/safe_globals.js',
113113
'lib/internal/net.js',
114114
'lib/internal/module.js',

‎test/es-module/test-esm-loader-modulemap.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { URL } = require('url');
1010
const Loader = require('internal/loader/Loader');
1111
const ModuleMap = require('internal/loader/ModuleMap');
1212
const ModuleJob = require('internal/loader/ModuleJob');
13-
const { createDynamicModule } = require('internal/loader/ModuleWrap');
13+
const createDynamicModule = require('internal/loader/CreateDynamicModule');
1414

1515
const stubModuleUrl = new URL('file://tmp/test');
1616
const stubModule = createDynamicModule(['default'], stubModuleUrl);

‎test/es-module/test-esm-loader-search.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
const common = require('../common');
77

8-
const search = require('internal/loader/search');
8+
const { search } = require('internal/loader/DefaultResolve');
99
const errors = require('internal/errors');
1010

1111
common.expectsError(

‎test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs

+12-12
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,9 @@ import module from 'module';
33
const builtins = new Set(
44
Object.keys(process.binding('natives')).filter(str =>
55
/^(?!(?:internal|node|v8)\/)/.test(str))
6-
)
6+
);
77

8-
export function resolve (specifier, base, defaultResolver) {
9-
if (builtins.has(specifier)) {
10-
return {
11-
url: `node:${specifier}`,
12-
format: 'dynamic'
13-
};
14-
}
15-
return defaultResolver(specifier, base);
16-
}
17-
18-
export async function dynamicInstantiate (url) {
8+
export function dynamicInstantiate(url) {
199
const builtinInstance = module._load(url.substr(5));
2010
const builtinExports = ['default', ...Object.keys(builtinInstance)];
2111
return {
@@ -27,3 +17,13 @@ export async function dynamicInstantiate (url) {
2717
}
2818
};
2919
}
20+
21+
export function resolve(specifier, base, defaultResolver) {
22+
if (builtins.has(specifier)) {
23+
return {
24+
url: `node:${specifier}`,
25+
format: 'dynamic'
26+
};
27+
}
28+
return defaultResolver(specifier, base);
29+
}

‎test/message/esm_display_syntax_error.out

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ file:///*/test/message/esm_display_syntax_error.mjs:3
33
await async () => 0;
44
^^^^^
55
SyntaxError: Unexpected reserved word
6-
at loaders.set (internal/loader/ModuleRequest.js:*:*)
6+
at translators.set (internal/loader/Translators.js:*:*)
77
at <anonymous>

‎test/message/esm_display_syntax_error_module.out

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2
33
await async () => 0;
44
^^^^^
55
SyntaxError: Unexpected reserved word
6-
at loaders.set (internal/loader/ModuleRequest.js:*:*)
6+
at translators.set (internal/loader/Translators.js:*:*)
77
at <anonymous>

‎test/parallel/test-internal-module-map-asserts.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const common = require('../common');
55
const assert = require('assert');
66
const ModuleMap = require('internal/loader/ModuleMap');
77

8-
// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
8+
// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
99
// values as url argument.
1010
{
1111
const errorReg = common.expectsError({

0 commit comments

Comments
 (0)
Please sign in to comment.