diff options
Diffstat (limited to 'lib/internal/modules/cjs/loader.js')
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 785 |
1 files changed, 785 insertions, 0 deletions
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js new file mode 100644 index 0000000000..a172266661 --- /dev/null +++ b/lib/internal/modules/cjs/loader.js @@ -0,0 +1,785 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { NativeModule } = require('internal/bootstrap/loaders'); +const util = require('util'); +const { decorateErrorStack } = require('internal/util'); +const { getURLFromFilePath } = require('internal/url'); +const vm = require('vm'); +const assert = require('assert').ok; +const fs = require('fs'); +const internalFS = require('internal/fs'); +const path = require('path'); +const { + internalModuleReadJSON, + internalModuleStat +} = process.binding('fs'); +const { safeGetenv } = process.binding('util'); +const { + makeRequireFunction, + requireDepth, + stripBOM, + stripShebang +} = require('internal/modules/cjs/helpers'); +const preserveSymlinks = !!process.binding('config').preserveSymlinks; +const experimentalModules = !!process.binding('config').experimentalModules; + +const { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_REQUIRE_ESM +} = require('internal/errors').codes; + +module.exports = Module; + +// these are below module.exports for the circular reference +const asyncESM = require('internal/process/esm_loader'); +const ModuleJob = require('internal/modules/esm/ModuleJob'); +const createDynamicModule = require('internal/modules/esm/CreateDynamicModule'); +const { + CHAR_UPPERCASE_A, + CHAR_LOWERCASE_A, + CHAR_UPPERCASE_Z, + CHAR_LOWERCASE_Z, + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH, + CHAR_COLON, + CHAR_DOT, + CHAR_UNDERSCORE, + CHAR_0, + CHAR_9, +} = require('internal/constants'); + +function stat(filename) { + filename = path.toNamespacedPath(filename); + const cache = stat.cache; + if (cache !== null) { + const result = cache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (cache !== null) cache.set(filename, result); + return result; +} +stat.cache = null; + +function updateChildren(parent, child, scan) { + var children = parent && parent.children; + if (children && !(scan && children.includes(child))) + children.push(child); +} + +function Module(id, parent) { + this.id = id; + this.exports = {}; + this.parent = parent; + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; +} + +const builtinModules = Object.keys(NativeModule._source) + .filter(NativeModule.nonInternalExists); + +Object.freeze(builtinModules); +Module.builtinModules = builtinModules; + +Module._cache = Object.create(null); +Module._pathCache = Object.create(null); +Module._extensions = Object.create(null); +var modulePaths = []; +Module.globalPaths = []; + +Module.wrap = function(script) { + return Module.wrapper[0] + script + Module.wrapper[1]; +}; + +Module.wrapper = [ + '(function (exports, require, module, __filename, __dirname) { ', + '\n});' +]; + +const debug = util.debuglog('module'); + +Module._debug = util.deprecate(debug, 'Module._debug is deprecated.', + 'DEP0077'); + +// given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.<ext>") +// -> a.<ext> +// +// require("a") +// -> a +// -> a.<ext> +// -> a/index.<ext> + +// check if the directory is a package.json dir +const packageMainCache = Object.create(null); + +function readPackage(requestPath) { + const entry = packageMainCache[requestPath]; + if (entry) + return entry; + + const jsonPath = path.resolve(requestPath, 'package.json'); + const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath)); + + if (json === undefined) { + return false; + } + + try { + var pkg = packageMainCache[requestPath] = JSON.parse(json).main; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } + return pkg; +} + +function tryPackage(requestPath, exts, isMain) { + var pkg = readPackage(requestPath); + + if (!pkg) return false; + + var filename = path.resolve(requestPath, pkg); + return tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new Map(); + +// check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (preserveSymlinks && !isMain) { + return rc === 0 && path.resolve(requestPath); + } + return rc === 0 && toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + [internalFS.realpathCacheKey]: realpathCache + }); +} + +// given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (var i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +var warned = false; +Module._findPath = function(request, paths, isMain) { + if (path.isAbsolute(request)) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + var cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : paths.join('\x00')); + var entry = Module._pathCache[cacheKey]; + if (entry) + return entry; + + var exts; + var trailingSlash = request.length > 0 && + request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = /(?:^|\/)\.?\.$/.test(request); + } + + // For each path + for (var i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + var basePath = path.resolve(curPath, request); + var filename; + + var rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (preserveSymlinks && !isMain) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // try it with each of the extensions + if (exts === undefined) + exts = Object.keys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = Object.keys(Module._extensions); + filename = tryPackage(basePath, exts, isMain); + if (!filename) { + filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain); + } + } + + if (filename) { + // Warn once if '.' resolved outside the module dir + if (request === '.' && i > 0) { + if (!warned) { + warned = true; + process.emitWarning( + 'warning: require(\'.\') resolved outside the package ' + + 'directory. This functionality is deprecated and will be removed ' + + 'soon.', + 'DeprecationWarning', 'DEP0019'); + } + } + + Module._pathCache[cacheKey] = filename; + return filename; + } + } + return false; +}; + +// 'node_modules' character codes reversed +var nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; +var nmLen = nmChars.length; +if (process.platform === 'win32') { + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // guarantee that 'from' is absolute. + from = path.resolve(from); + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + + // return root node_modules when path is 'D:\\'. + // path.resolve will make sure from.length >=3 in Windows. + if (from.charCodeAt(from.length - 1) === CHAR_BACKWARD_SLASH && + from.charCodeAt(from.length - 2) === CHAR_COLON) + return [from + 'node_modules']; + + const paths = []; + var p = 0; + var last = from.length; + for (var i = from.length - 1; i >= 0; --i) { + const code = from.charCodeAt(i); + // The path segment separator check ('\' and '/') was used to get + // node_modules path for every path segment. + // Use colon as an extra condition since we can get node_modules + // path for drive root like 'C:\node_modules' and don't need to + // parse drive name. + if (code === CHAR_BACKWARD_SLASH || + code === CHAR_FORWARD_SLASH || + code === CHAR_COLON) { + if (p !== nmLen) + paths.push(from.slice(0, last) + '\\node_modules'); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + return paths; + }; +} else { // posix + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // guarantee that 'from' is absolute. + from = path.resolve(from); + // Return early not only to avoid unnecessary work, but to *avoid* returning + // an array of two items for a root: [ '//node_modules', '/node_modules' ] + if (from === '/') + return ['/node_modules']; + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + const paths = []; + var p = 0; + var last = from.length; + for (var i = from.length - 1; i >= 0; --i) { + const code = from.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + if (p !== nmLen) + paths.push(from.slice(0, last) + '/node_modules'); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + // Append /node_modules to handle root paths. + paths.push('/node_modules'); + + return paths; + }; +} + + +// 'index.' character codes +var indexChars = [ 105, 110, 100, 101, 120, 46 ]; +var indexLen = indexChars.length; +Module._resolveLookupPaths = function(request, parent, newReturn) { + if (NativeModule.nonInternalExists(request)) { + debug('looking for %j in []', request); + return (newReturn ? null : [request, []]); + } + + // Check for relative path + if (request.length < 2 || + request.charCodeAt(0) !== CHAR_DOT || + (request.charCodeAt(1) !== CHAR_DOT && + request.charCodeAt(1) !== CHAR_FORWARD_SLASH)) { + var paths = modulePaths; + if (parent) { + if (!parent.paths) + paths = parent.paths = []; + else + paths = parent.paths.concat(paths); + } + + // Maintain backwards compat with certain broken uses of require('.') + // by putting the module's directory in front of the lookup paths. + if (request === '.') { + if (parent && parent.filename) { + paths.unshift(path.dirname(parent.filename)); + } else { + paths.unshift(path.resolve(request)); + } + } + + debug('looking for %j in %j', request, paths); + return (newReturn ? (paths.length > 0 ? paths : null) : [request, paths]); + } + + // with --eval, parent.id is not set and parent.filename is null + if (!parent || !parent.id || !parent.filename) { + // make require('./path/to/foo') work - normally the path is taken + // from realpath(__filename) but with eval there is no filename + var mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths); + + debug('looking for %j in %j', request, mainPaths); + return (newReturn ? mainPaths : [request, mainPaths]); + } + + // Is the parent an index module? + // We can assume the parent has a valid extension, + // as it already has been accepted as a module. + const base = path.basename(parent.filename); + var parentIdPath; + if (base.length > indexLen) { + var i = 0; + for (; i < indexLen; ++i) { + if (indexChars[i] !== base.charCodeAt(i)) + break; + } + if (i === indexLen) { + // We matched 'index.', let's validate the rest + for (; i < base.length; ++i) { + const code = base.charCodeAt(i); + if (code !== CHAR_UNDERSCORE && + (code < CHAR_0 || code > CHAR_9) && + (code < CHAR_UPPERCASE_A || code > CHAR_UPPERCASE_Z) && + (code < CHAR_LOWERCASE_A || code > CHAR_LOWERCASE_Z)) + break; + } + if (i === base.length) { + // Is an index module + parentIdPath = parent.id; + } else { + // Not an index module + parentIdPath = path.dirname(parent.id); + } + } else { + // Not an index module + parentIdPath = path.dirname(parent.id); + } + } else { + // Not an index module + parentIdPath = path.dirname(parent.id); + } + var id = path.resolve(parentIdPath, request); + + // make sure require('./path') and require('path') get distinct ids, even + // when called from the toplevel js file + if (parentIdPath === '.' && id.indexOf('/') === -1) { + id = './' + id; + } + + debug('RELATIVE: requested: %s set ID to: %s from %s', request, id, + parent.id); + + var parentDir = [path.dirname(parent.filename)]; + debug('looking for %j in %j', id, parentDir); + return (newReturn ? parentDir : [id, parentDir]); +}; + +// Check the cache for the requested file. +// 1. If a module already exists in the cache: return its exports object. +// 2. If the module is native: call `NativeModule.require()` with the +// filename and return the result. +// 3. Otherwise, create a new module for the file and save it to the cache. +// Then have it load the file contents before returning its exports +// object. +Module._load = function(request, parent, isMain) { + if (parent) { + debug('Module._load REQUEST %s parent: %s', request, parent.id); + } + + if (experimentalModules && isMain) { + asyncESM.loaderPromise.then((loader) => { + return loader.import(getURLFromFilePath(request).pathname); + }) + .catch((e) => { + decorateErrorStack(e); + console.error(e); + process.exit(1); + }); + return; + } + + var filename = Module._resolveFilename(request, parent, isMain); + + var cachedModule = Module._cache[filename]; + if (cachedModule) { + updateChildren(parent, cachedModule, true); + return cachedModule.exports; + } + + if (NativeModule.nonInternalExists(filename)) { + debug('load native module %s', request); + return NativeModule.require(filename); + } + + // Don't call updateChildren(), Module constructor already does. + var module = new Module(filename, parent); + + if (isMain) { + process.mainModule = module; + module.id = '.'; + } + + Module._cache[filename] = module; + + tryModuleLoad(module, filename); + + return module.exports; +}; + +function tryModuleLoad(module, filename) { + var threw = true; + try { + module.load(filename); + threw = false; + } finally { + if (threw) { + delete Module._cache[filename]; + } + } +} + +Module._resolveFilename = function(request, parent, isMain, options) { + if (NativeModule.nonInternalExists(request)) { + return request; + } + + var paths; + + if (typeof options === 'object' && options !== null && + Array.isArray(options.paths)) { + const fakeParent = new Module('', null); + + paths = []; + + for (var i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent, true); + + if (!paths.includes(path)) + paths.push(path); + + for (var j = 0; j < lookupPaths.length; j++) { + if (!paths.includes(lookupPaths[j])) + paths.push(lookupPaths[j]); + } + } + } else { + paths = Module._resolveLookupPaths(request, parent, true); + } + + // look up the filename first, since that's the cache key. + var filename = Module._findPath(request, paths, isMain); + if (!filename) { + var err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + throw err; + } + return filename; +}; + + +// Given a file name, pass it to the proper extension handler. +Module.prototype.load = function(filename) { + debug('load %j for module %j', filename, this.id); + + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); + + var extension = path.extname(filename) || '.js'; + if (!Module._extensions[extension]) extension = '.js'; + Module._extensions[extension](this, filename); + this.loaded = true; + + if (experimentalModules) { + const ESMLoader = asyncESM.ESMLoader; + const url = getURLFromFilePath(filename); + const urlString = `${url}`; + const exports = this.exports; + if (ESMLoader.moduleMap.has(urlString) !== true) { + ESMLoader.moduleMap.set( + urlString, + new ModuleJob(ESMLoader, url, async () => { + const ctx = createDynamicModule( + ['default'], url); + ctx.reflect.exports.default.set(exports); + return ctx; + }) + ); + } else { + const job = ESMLoader.moduleMap.get(urlString); + if (job.reflect) + job.reflect.exports.default.set(exports); + } + } +}; + + +// Loads a module at the given file path. Returns that module's +// `exports` property. +Module.prototype.require = function(id) { + if (typeof id !== 'string') { + throw new ERR_INVALID_ARG_TYPE('id', 'string', id); + } + if (id === '') { + throw new ERR_INVALID_ARG_VALUE('id', id, + 'must be a non-empty string'); + } + return Module._load(id, this, /* isMain */ false); +}; + + +// Resolved path to process.argv[1] will be lazily placed here +// (needed for setting breakpoint when called with --inspect-brk) +var resolvedArgv; + + +// Run the file contents in the correct scope or sandbox. Expose +// the correct helper variables (require, module, exports) to +// the file. +// Returns exception, if any. +Module.prototype._compile = function(content, filename) { + + content = stripShebang(content); + + // create wrapper function + var wrapper = Module.wrap(content); + + var compiledWrapper = vm.runInThisContext(wrapper, { + filename: filename, + lineOffset: 0, + displayErrors: true + }); + + var inspectorWrapper = null; + if (process._breakFirstLine && process._eval == null) { + if (!resolvedArgv) { + // we enter the repl if we're not given a filename argument. + if (process.argv[1]) { + resolvedArgv = Module._resolveFilename(process.argv[1], null, false); + } else { + resolvedArgv = 'repl'; + } + } + + // Set breakpoint on module start + if (filename === resolvedArgv) { + delete process._breakFirstLine; + inspectorWrapper = process.binding('inspector').callAndPauseOnStart; + } + } + var dirname = path.dirname(filename); + var require = makeRequireFunction(this); + var depth = requireDepth; + if (depth === 0) stat.cache = new Map(); + var result; + if (inspectorWrapper) { + result = inspectorWrapper(compiledWrapper, this.exports, this.exports, + require, this, filename, dirname); + } else { + result = compiledWrapper.call(this.exports, this.exports, require, this, + filename, dirname); + } + if (depth === 0) stat.cache = null; + return result; +}; + + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + var content = fs.readFileSync(filename, 'utf8'); + module._compile(stripBOM(content), filename); +}; + + +// Native extension for .json +Module._extensions['.json'] = function(module, filename) { + var content = fs.readFileSync(filename, 'utf8'); + try { + module.exports = JSON.parse(stripBOM(content)); + } catch (err) { + err.message = filename + ': ' + err.message; + throw err; + } +}; + + +//Native extension for .node +Module._extensions['.node'] = function(module, filename) { + return process.dlopen(module, path.toNamespacedPath(filename)); +}; + +if (experimentalModules) { + Module._extensions['.mjs'] = function(module, filename) { + throw new ERR_REQUIRE_ESM(filename); + }; +} + +// bootstrap main module. +Module.runMain = function() { + // Load the main module--the command line argument. + Module._load(process.argv[1], null, true); + // Handle any nextTicks added in the first tick of the program + process._tickCallback(); +}; + +Module._initPaths = function() { + const isWindows = process.platform === 'win32'; + + var homeDir; + var nodePath; + if (isWindows) { + homeDir = process.env.USERPROFILE; + nodePath = process.env.NODE_PATH; + } else { + homeDir = safeGetenv('HOME'); + nodePath = safeGetenv('NODE_PATH'); + } + + // $PREFIX/lib/node, where $PREFIX is the root of the Node.js installation. + var prefixDir; + // process.execPath is $PREFIX/bin/node except on Windows where it is + // $PREFIX\node.exe. + if (isWindows) { + prefixDir = path.resolve(process.execPath, '..'); + } else { + prefixDir = path.resolve(process.execPath, '..', '..'); + } + var paths = [path.resolve(prefixDir, 'lib', 'node')]; + + if (homeDir) { + paths.unshift(path.resolve(homeDir, '.node_libraries')); + paths.unshift(path.resolve(homeDir, '.node_modules')); + } + + if (nodePath) { + paths = nodePath.split(path.delimiter).filter(function(path) { + return !!path; + }).concat(paths); + } + + modulePaths = paths; + + // clone as a shallow copy, for introspection. + Module.globalPaths = modulePaths.slice(0); +}; + +Module._preloadModules = function(requests) { + if (!Array.isArray(requests)) + return; + + // Preloaded modules have a dummy parent module which is deemed to exist + // in the current working directory. This seeds the search path for + // preloaded modules. + var parent = new Module('internal/preload', null); + try { + parent.paths = Module._nodeModulePaths(process.cwd()); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + } + for (var n = 0; n < requests.length; n++) + parent.require(requests[n]); +}; + +Module._initPaths(); + +// backwards compatibility +Module.Module = Module; |