module: refactor loader

PR-URL: https://github.com/nodejs/node/pull/16874
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
This commit is contained in:
Gus Caplan 2017-11-26 17:12:09 -06:00 committed by Bradley Farias
parent 85739b6c5b
commit 921fb84687
14 changed files with 270 additions and 274 deletions

View File

@ -1,9 +1,6 @@
'use strict';
const {
ModuleWrap,
setImportModuleDynamicallyCallback
} = internalBinding('module_wrap');
const { ModuleWrap } = internalBinding('module_wrap');
const debug = require('util').debuglog('esm');
const ArrayJoin = Function.call.bind(Array.prototype.join);
const ArrayMap = Function.call.bind(Array.prototype.map);
@ -60,8 +57,4 @@ const createDynamicModule = (exports, url = '', evaluate) => {
};
};
module.exports = {
createDynamicModule,
setImportModuleDynamicallyCallback,
ModuleWrap
};
module.exports = createDynamicModule;

View File

@ -0,0 +1,84 @@
'use strict';
const { URL } = require('url');
const CJSmodule = require('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 errors = require('internal/errors');
const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
const StringStartsWith = Function.call.bind(String.prototype.startsWith);
const realpathCache = new Map();
function search(target, base) {
if (base === undefined) {
// We cannot search without a base.
throw new errors.Error('ERR_MISSING_MODULE', target);
}
try {
return moduleWrapResolve(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;
}
}
const extensionFormatMap = {
__proto__: null,
'.mjs': 'esm',
'.json': 'json',
'.node': 'addon',
'.js': 'commonjs'
};
function resolve(specifier, parentURL) {
if (NativeModule.nonInternalExists(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
let url;
try {
url = search(specifier, parentURL);
} catch (e) {
if (typeof e.message === 'string' &&
StringStartsWith(e.message, 'Cannot find module'))
e.code = 'MODULE_NOT_FOUND';
throw e;
}
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);
return { url: `${url}`, format: extensionFormatMap[ext] || ext };
}
module.exports = resolve;
// exported for tests
module.exports.search = search;

View File

@ -2,16 +2,16 @@
const path = require('path');
const { getURLFromFilePath, URL } = require('internal/url');
const {
createDynamicModule,
setImportModuleDynamicallyCallback
} = require('internal/loader/ModuleWrap');
const errors = require('internal/errors');
const ModuleMap = require('internal/loader/ModuleMap');
const ModuleJob = require('internal/loader/ModuleJob');
const ModuleRequest = require('internal/loader/ModuleRequest');
const errors = require('internal/errors');
const defaultResolve = require('internal/loader/DefaultResolve');
const createDynamicModule = require('internal/loader/CreateDynamicModule');
const translators = require('internal/loader/Translators');
const { setImportModuleDynamicallyCallback } = internalBinding('module_wrap');
const FunctionBind = Function.call.bind(Function.prototype.bind);
const debug = require('util').debuglog('esm');
// Returns a file URL for the current working directory.
@ -40,105 +40,101 @@ function normalizeReferrerURL(referrer) {
* the main module and everything in its dependency graph. */
class Loader {
constructor(base = getURLStringForCwd()) {
if (typeof base !== 'string') {
if (typeof base !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
}
this.moduleMap = new ModuleMap();
this.base = base;
// methods which translate input code or other information
// into es modules
this.translators = translators;
// registry of loaded modules, akin to `require.cache`
this.moduleMap = new ModuleMap();
// The resolver has the signature
// (specifier : string, parentURL : string, defaultResolve)
// -> Promise<{ url : string,
// format: anything in Loader.validFormats }>
// -> Promise<{ url : string, format: string }>
// where defaultResolve is ModuleRequest.resolve (having the same
// signature itself).
// If `.format` on the returned value is 'dynamic', .dynamicInstantiate
// will be used as described below.
this.resolver = ModuleRequest.resolve;
// This hook is only called when resolve(...).format is 'dynamic' and has
// the signature
this._resolve = defaultResolve;
// This hook is only called when resolve(...).format is 'dynamic' and
// has the signature
// (url : string) -> Promise<{ exports: { ... }, execute: function }>
// Where `exports` is an object whose property names define the exported
// names of the generated module. `execute` is a function that receives
// an object with the same keys as `exports`, whose values are get/set
// functions for the actual exported values.
this.dynamicInstantiate = undefined;
this._dynamicInstantiate = undefined;
}
hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) {
// Use .bind() to avoid giving access to the Loader instance when it is
// called as this.resolver(...);
this.resolver = resolve.bind(null);
this.dynamicInstantiate = dynamicInstantiate;
}
// Typechecking wrapper around .resolver().
async resolve(specifier, parentURL = this.base) {
if (typeof parentURL !== 'string') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'parentURL', 'string');
}
if (typeof parentURL !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURL', 'string');
const { url, format } = await this.resolver(specifier, parentURL,
ModuleRequest.resolve);
const { url, format } =
await this._resolve(specifier, parentURL, defaultResolve);
if (!Loader.validFormats.includes(format)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format',
Loader.validFormats);
}
if (typeof url !== 'string') {
if (typeof url !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
}
if (format === 'builtin') {
if (typeof format !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format', 'string');
if (format === 'builtin')
return { url: `node:${url}`, format };
}
if (format !== 'dynamic') {
if (!ModuleRequest.loaders.has(format)) {
throw new errors.Error('ERR_UNKNOWN_MODULE_FORMAT', format);
}
if (!url.startsWith('file:')) {
throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:');
}
}
if (format !== 'dynamic' && !url.startsWith('file:'))
throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:');
return { url, format };
}
// May create a new ModuleJob instance if one did not already exist.
async import(specifier, parent = this.base) {
const job = await this.getModuleJob(specifier, parent);
const module = await job.run();
return module.namespace();
}
hook({ resolve, dynamicInstantiate }) {
// Use .bind() to avoid giving access to the Loader instance when called.
if (resolve !== undefined)
this._resolve = FunctionBind(resolve, null);
if (dynamicInstantiate !== undefined)
this._dynamicInstantiate = FunctionBind(dynamicInstantiate, null);
}
async getModuleJob(specifier, parentURL = this.base) {
const { url, format } = await this.resolve(specifier, parentURL);
let job = this.moduleMap.get(url);
if (job === undefined) {
let loaderInstance;
if (format === 'dynamic') {
const { dynamicInstantiate } = this;
if (typeof dynamicInstantiate !== 'function') {
throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK');
}
if (job !== undefined)
return job;
loaderInstance = async (url) => {
const { exports, execute } = await dynamicInstantiate(url);
return createDynamicModule(exports, url, (reflect) => {
debug(`Loading custom loader ${url}`);
execute(reflect.exports);
});
};
} else {
loaderInstance = ModuleRequest.loaders.get(format);
}
job = new ModuleJob(this, url, loaderInstance);
this.moduleMap.set(url, job);
let loaderInstance;
if (format === 'dynamic') {
if (typeof this._dynamicInstantiate !== 'function')
throw new errors.Error('ERR_MISSING_DYNAMIC_INTSTANTIATE_HOOK');
loaderInstance = async (url) => {
debug(`Translating dynamic ${url}`);
const { exports, execute } = await this._dynamicInstantiate(url);
return createDynamicModule(exports, url, (reflect) => {
debug(`Loading dynamic ${url}`);
execute(reflect.exports);
});
};
} else {
if (!translators.has(format))
throw new errors.RangeError('ERR_UNKNOWN_MODULE_FORMAT', format);
loaderInstance = translators.get(format);
}
return job;
}
async import(specifier, parentURL = this.base) {
const job = await this.getModuleJob(specifier, parentURL);
const module = await job.run();
return module.namespace();
job = new ModuleJob(this, url, loaderInstance);
this.moduleMap.set(url, job);
return job;
}
static registerImportDynamicallyCallback(loader) {
@ -147,6 +143,6 @@ class Loader {
});
}
}
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];
Object.setPrototypeOf(Loader.prototype, null);
module.exports = Loader;

View File

@ -1,138 +0,0 @@
'use strict';
const fs = require('fs');
const internalCJSModule = require('internal/module');
const CJSModule = require('module');
const internalURLModule = require('internal/url');
const internalFS = require('internal/fs');
const NativeModule = require('native_module');
const { extname, _makeLong } = require('path');
const { URL } = require('url');
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();
const loaders = new Map();
exports.loaders = loaders;
// Strategy for loading a standard JavaScript module
loaders.set('esm', async (url) => {
const source = `${await asyncReadFile(new URL(url))}`;
debug(`Loading StandardModule ${url}`);
return {
module: new ModuleWrap(internalCJSModule.stripShebang(source), url),
reflect: undefined
};
});
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
loaders.set('cjs', async (url) => {
const pathname = internalURLModule.getPathFromURL(new URL(url));
const module = CJSModule._cache[
isWindows ? pathname.replace(winSepRegEx, '\\') : pathname];
if (module && module.loaded) {
const ctx = createDynamicModule(['default'], url, undefined);
ctx.reflect.exports.default.set(module.exports);
return ctx;
}
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading CJSModule ${url}`);
CJSModule._load(pathname);
});
});
// Strategy for loading a node builtin CommonJS module that isn't
// through normal resolution
loaders.set('builtin', async (url) => {
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading BuiltinModule ${url}`);
const exports = NativeModule.require(url.substr(5));
reflect.exports.default.set(exports);
});
});
loaders.set('addon', async (url) => {
const ctx = createDynamicModule(['default'], url, (reflect) => {
debug(`Loading NativeModule ${url}`);
const module = { exports: {} };
const pathname = internalURLModule.getPathFromURL(new URL(url));
process.dlopen(module, _makeLong(pathname));
reflect.exports.default.set(module.exports);
});
return ctx;
});
loaders.set('json', async (url) => {
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading JSONModule ${url}`);
const pathname = internalURLModule.getPathFromURL(new URL(url));
const content = fs.readFileSync(pathname, 'utf8');
try {
const exports = JSON.parse(internalCJSModule.stripBOM(content));
reflect.exports.default.set(exports);
} catch (err) {
err.message = pathname + ': ' + err.message;
throw err;
}
});
});
exports.resolve = (specifier, parentURL) => {
if (NativeModule.nonInternalExists(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
let url;
try {
url = search(specifier, parentURL);
} catch (e) {
if (e.message && e.message.startsWith('Cannot find module'))
e.code = 'MODULE_NOT_FOUND';
throw e;
}
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);
switch (ext) {
case '.mjs':
return { url: `${url}`, format: 'esm' };
case '.json':
return { url: `${url}`, format: 'json' };
case '.node':
return { url: `${url}`, format: 'addon' };
case '.js':
return { url: `${url}`, format: 'cjs' };
default:
throw new errors.Error('ERR_UNKNOWN_FILE_EXTENSION',
internalURLModule.getPathFromURL(url));
}
};

View File

@ -0,0 +1,92 @@
'use strict';
const { ModuleWrap } = internalBinding('module_wrap');
const NativeModule = require('native_module');
const internalCJSModule = require('internal/module');
const CJSModule = require('module');
const internalURLModule = require('internal/url');
const createDynamicModule = require('internal/loader/CreateDynamicModule');
const fs = require('fs');
const { _makeLong } = require('path');
const { SafeMap } = require('internal/safe_globals');
const { URL } = require('url');
const debug = require('util').debuglog('esm');
const readFileAsync = require('util').promisify(fs.readFile);
const readFileSync = fs.readFileSync;
const StringReplace = Function.call.bind(String.prototype.replace);
const JsonParse = JSON.parse;
const translators = new SafeMap();
module.exports = translators;
// Stragety for loading a standard JavaScript module
translators.set('esm', async (url) => {
const source = `${await readFileAsync(new URL(url))}`;
debug(`Translating StandardModule ${url}`);
return {
module: new ModuleWrap(internalCJSModule.stripShebang(source), url),
reflect: undefined
};
});
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('commonjs', async (url) => {
debug(`Translating CJSModule ${url}`);
const pathname = internalURLModule.getPathFromURL(new URL(url));
const module = CJSModule._cache[
isWindows ? StringReplace(pathname, winSepRegEx, '\\') : pathname];
if (module && module.loaded) {
const ctx = createDynamicModule(['default'], url);
ctx.reflect.exports.default.set(module.exports);
return ctx;
}
return createDynamicModule(['default'], url, () => {
debug(`Loading CJSModule ${url}`);
// we don't care about the return val of _load here because Module#load
// will handle it for us by checking the loader registry and filling the
// exports like above
CJSModule._load(pathname);
});
});
// Strategy for loading a node builtin CommonJS module that isn't
// through normal resolution
translators.set('builtin', async (url) => {
debug(`Translating BuiltinModule ${url}`);
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading BuiltinModule ${url}`);
const exports = NativeModule.require(url.slice(5));
reflect.exports.default.set(exports);
});
});
// Stragety for loading a node native module
translators.set('addon', async (url) => {
debug(`Translating NativeModule ${url}`);
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading NativeModule ${url}`);
const module = { exports: {} };
const pathname = internalURLModule.getPathFromURL(new URL(url));
process.dlopen(module, _makeLong(pathname));
reflect.exports.default.set(module.exports);
});
});
// Stragety for loading a JSON file
translators.set('json', async (url) => {
debug(`Translating JSONModule ${url}`);
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading JSONModule ${url}`);
const pathname = internalURLModule.getPathFromURL(new URL(url));
const content = readFileSync(pathname, 'utf8');
try {
const exports = JsonParse(internalCJSModule.stripBOM(content));
reflect.exports.default.set(exports);
} catch (err) {
err.message = pathname + ': ' + err.message;
throw err;
}
});
});

View File

@ -1,31 +0,0 @@
'use strict';
const { URL } = require('url');
const CJSmodule = require('module');
const errors = require('internal/errors');
const { resolve } = internalBinding('module_wrap');
module.exports = (target, base) => {
if (base === undefined) {
// We cannot search without a base.
throw new errors.Error('ERR_MISSING_MODULE', target);
}
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;
}
};

View File

@ -45,7 +45,7 @@ module.exports = Module;
// these are below module.exports for the circular reference
const Loader = require('internal/loader/Loader');
const ModuleJob = require('internal/loader/ModuleJob');
const { createDynamicModule } = require('internal/loader/ModuleWrap');
const createDynamicModule = require('internal/loader/CreateDynamicModule');
let ESMLoader;
function stat(filename) {

View File

@ -101,11 +101,11 @@
'lib/internal/inspector_async_hook.js',
'lib/internal/linkedlist.js',
'lib/internal/loader/Loader.js',
'lib/internal/loader/ModuleMap.js',
'lib/internal/loader/CreateDynamicModule.js',
'lib/internal/loader/DefaultResolve.js',
'lib/internal/loader/ModuleJob.js',
'lib/internal/loader/ModuleWrap.js',
'lib/internal/loader/ModuleRequest.js',
'lib/internal/loader/search.js',
'lib/internal/loader/ModuleMap.js',
'lib/internal/loader/Translators.js',
'lib/internal/safe_globals.js',
'lib/internal/net.js',
'lib/internal/module.js',

View File

@ -10,7 +10,7 @@ const { URL } = require('url');
const Loader = require('internal/loader/Loader');
const ModuleMap = require('internal/loader/ModuleMap');
const ModuleJob = require('internal/loader/ModuleJob');
const { createDynamicModule } = require('internal/loader/ModuleWrap');
const createDynamicModule = require('internal/loader/CreateDynamicModule');
const stubModuleUrl = new URL('file://tmp/test');
const stubModule = createDynamicModule(['default'], stubModuleUrl);

View File

@ -5,7 +5,7 @@
const common = require('../common');
const search = require('internal/loader/search');
const { search } = require('internal/loader/DefaultResolve');
const errors = require('internal/errors');
common.expectsError(

View File

@ -3,19 +3,9 @@ import module from 'module';
const builtins = new Set(
Object.keys(process.binding('natives')).filter(str =>
/^(?!(?:internal|node|v8)\/)/.test(str))
)
);
export function resolve (specifier, base, defaultResolver) {
if (builtins.has(specifier)) {
return {
url: `node:${specifier}`,
format: 'dynamic'
};
}
return defaultResolver(specifier, base);
}
export async function dynamicInstantiate (url) {
export function dynamicInstantiate(url) {
const builtinInstance = module._load(url.substr(5));
const builtinExports = ['default', ...Object.keys(builtinInstance)];
return {
@ -27,3 +17,13 @@ export async function dynamicInstantiate (url) {
}
};
}
export function resolve(specifier, base, defaultResolver) {
if (builtins.has(specifier)) {
return {
url: `node:${specifier}`,
format: 'dynamic'
};
}
return defaultResolver(specifier, base);
}

View File

@ -3,5 +3,5 @@ file:///*/test/message/esm_display_syntax_error.mjs:3
await async () => 0;
^^^^^
SyntaxError: Unexpected reserved word
at loaders.set (internal/loader/ModuleRequest.js:*:*)
at translators.set (internal/loader/Translators.js:*:*)
at <anonymous>

View File

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

View File

@ -5,7 +5,7 @@ const common = require('../common');
const assert = require('assert');
const ModuleMap = require('internal/loader/ModuleMap');
// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
// values as url argument.
{
const errorReg = common.expectsError({