Skip to content

Commit

Permalink
module: refactor loader
Browse files Browse the repository at this point in the history
PR-URL: nodejs#16874
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
  • Loading branch information
devsnek authored and bmeck committed Jan 15, 2018
1 parent 85739b6 commit 921fb84
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 275 deletions.
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -60,8 +57,4 @@ const createDynamicModule = (exports, url = '', evaluate) => {
};
};

module.exports = {
createDynamicModule,
setImportModuleDynamicallyCallback,
ModuleWrap
};
module.exports = createDynamicModule;
84 changes: 84 additions & 0 deletions lib/internal/loader/DefaultResolve.js
Original file line number Diff line number Diff line change
@@ -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;
146 changes: 71 additions & 75 deletions lib/internal/loader/Loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

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;
this._dynamicInstantiate = undefined;
}

// Typechecking wrapper around .resolver().
async resolve(specifier, parentURL = this.base) {
if (typeof parentURL !== 'string') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'parentURL', 'string');
}

const { url, format } = await this.resolver(specifier, parentURL,
ModuleRequest.resolve);
if (typeof parentURL !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURL', 'string');

if (!Loader.validFormats.includes(format)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format',
Loader.validFormats);
}
const { url, format } =
await this._resolve(specifier, parentURL, defaultResolve);

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

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);
if (job !== undefined)
return 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) {
Expand All @@ -147,6 +143,6 @@ class Loader {
});
}
}
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];

Object.setPrototypeOf(Loader.prototype, null);
module.exports = Loader;
Loading

0 comments on commit 921fb84

Please sign in to comment.