Module = require 'module' path = require 'path' semver = require 'semver' # Extend semver.Range to memoize matched versions for speed class Range extends semver.Range constructor: -> super @matchedVersions = new Set() @unmatchedVersions = new Set() test: (version) -> return true if @matchedVersions.has(version) return false if @unmatchedVersions.has(version) matches = super if matches @matchedVersions.add(version) else @unmatchedVersions.add(version) matches nativeModules = process.binding('natives') cache = builtins: {} debug: false dependencies: {} extensions: {} folders: {} ranges: {} registered: false resourcePath: null resourcePathWithTrailingSlash: null # isAbsolute is inlined from fs-plus so that fs-plus itself can be required # from this cache. if process.platform is 'win32' isAbsolute = (pathToCheck) -> pathToCheck and (pathToCheck[1] is ':' or (pathToCheck[0] is '\\' and pathToCheck[1] is '\\')) else isAbsolute = (pathToCheck) -> pathToCheck and pathToCheck[0] is '/' isCorePath = (pathToCheck) -> pathToCheck.startsWith(cache.resourcePathWithTrailingSlash) loadDependencies = (modulePath, rootPath, rootMetadata, moduleCache) -> fs = require 'fs-plus' for childPath in fs.listSync(path.join(modulePath, 'node_modules')) continue if path.basename(childPath) is '.bin' continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath)) childMetadataPath = path.join(childPath, 'package.json') continue unless fs.isFileSync(childMetadataPath) childMetadata = JSON.parse(fs.readFileSync(childMetadataPath)) if childMetadata?.version try mainPath = require.resolve(childPath) catch error mainPath = null if mainPath moduleCache.dependencies.push name: childMetadata.name version: childMetadata.version path: path.relative(rootPath, mainPath) loadDependencies(childPath, rootPath, rootMetadata, moduleCache) return loadFolderCompatibility = (modulePath, rootPath, rootMetadata, moduleCache) -> fs = require 'fs-plus' metadataPath = path.join(modulePath, 'package.json') return unless fs.isFileSync(metadataPath) dependencies = JSON.parse(fs.readFileSync(metadataPath))?.dependencies ? {} for name, version of dependencies try new Range(version) catch error delete dependencies[name] onDirectory = (childPath) -> path.basename(childPath) isnt 'node_modules' extensions = ['.js', '.coffee', '.json', '.node'] paths = {} onFile = (childPath) -> if path.extname(childPath) in extensions relativePath = path.relative(rootPath, path.dirname(childPath)) paths[relativePath] = true fs.traverseTreeSync(modulePath, onFile, onDirectory) paths = Object.keys(paths) if paths.length > 0 and Object.keys(dependencies).length > 0 moduleCache.folders.push({paths, dependencies}) for childPath in fs.listSync(path.join(modulePath, 'node_modules')) continue if path.basename(childPath) is '.bin' continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath)) loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache) return loadExtensions = (modulePath, rootPath, rootMetadata, moduleCache) -> fs = require 'fs-plus' extensions = ['.js', '.coffee', '.json', '.node'] nodeModulesPath = path.join(rootPath, 'node_modules') onFile = (filePath) -> filePath = path.relative(rootPath, filePath) segments = filePath.split(path.sep) return if 'test' in segments return if 'tests' in segments return if 'spec' in segments return if 'specs' in segments return if segments.length > 1 and not (segments[0] in ['lib', 'node_modules', 'src', 'static', 'vendor']) extension = path.extname(filePath) if extension in extensions moduleCache.extensions[extension] ?= [] moduleCache.extensions[extension].push(filePath) onDirectory = (childPath) -> # Don't include extensions from bundled packages # These are generated and stored in the package's own metadata cache if rootMetadata.name is 'nylas' parentPath = path.dirname(childPath) if parentPath is nodeModulesPath packageName = path.basename(childPath) return false if rootMetadata.packageDependencies?.hasOwnProperty(packageName) true fs.traverseTreeSync(rootPath, onFile, onDirectory) return satisfies = (version, rawRange) -> unless parsedRange = cache.ranges[rawRange] parsedRange = new Range(rawRange) cache.ranges[rawRange] = parsedRange parsedRange.test(version) resolveFilePath = (relativePath, parentModule) -> return unless relativePath return unless parentModule?.filename return unless relativePath[0] is '.' or isAbsolute(relativePath) resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath) return unless isCorePath(resolvedPath) extension = path.extname(resolvedPath) if extension return resolvedPath if cache.extensions[extension]?.has(resolvedPath) else for extension, paths of cache.extensions resolvedPathWithExtension = "#{resolvedPath}#{extension}" return resolvedPathWithExtension if paths.has(resolvedPathWithExtension) return resolveModulePath = (relativePath, parentModule) -> return unless relativePath return unless parentModule?.filename return if nativeModules.hasOwnProperty(relativePath) return if relativePath[0] is '.' return if isAbsolute(relativePath) folderPath = path.dirname(parentModule.filename) range = cache.folders[folderPath]?[relativePath] unless range? if builtinPath = cache.builtins[relativePath] return builtinPath else return candidates = cache.dependencies[relativePath] return unless candidates? for version, resolvedPath of candidates if Module._cache.hasOwnProperty(resolvedPath) or isCorePath(resolvedPath) return resolvedPath if satisfies(version, range) return registerBuiltins = (devMode) -> electronRoot = path.join(process.resourcesPath, 'atom.asar') commonRoot = path.join(electronRoot, 'common', 'api', 'lib') commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell'] for builtin in commonBuiltins cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js") rendererRoot = path.join(electronRoot, 'renderer', 'api', 'lib') rendererBuiltins = ['ipc', 'remote'] for builtin in rendererBuiltins cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") if cache.debug cache.findPathCount = 0 cache.findPathTime = 0 cache.loadCount = 0 cache.requireTime = 0 global.moduleCache = cache originalLoad = Module::load Module::load = -> cache.loadCount++ originalLoad.apply(this, arguments) originalRequire = Module::require Module::require = -> startTime = Date.now() exports = originalRequire.apply(this, arguments) cache.requireTime += Date.now() - startTime exports originalFindPath = Module._findPath Module._findPath = (request, paths) -> cacheKey = JSON.stringify({request, paths}) cache.findPathCount++ unless Module._pathCache[cacheKey] startTime = Date.now() foundPath = originalFindPath.apply(global, arguments) cache.findPathTime += Date.now() - startTime foundPath exports.create = (modulePath) -> fs = require 'fs-plus' modulePath = fs.realpathSync(modulePath) metadataPath = path.join(modulePath, 'package.json') metadata = JSON.parse(fs.readFileSync(metadataPath)) moduleCache = version: 1 dependencies: [] extensions: {} folders: [] loadDependencies(modulePath, modulePath, metadata, moduleCache) loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache) loadExtensions(modulePath, modulePath, metadata, moduleCache) metadata._nylasModuleCache = moduleCache fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)) return exports.register = ({resourcePath, devMode}={}) -> return if cache.registered originalResolveFilename = Module._resolveFilename Module._resolveFilename = (relativePath, parentModule) -> resolvedPath = resolveModulePath(relativePath, parentModule) resolvedPath ?= resolveFilePath(relativePath, parentModule) resolvedPath ? originalResolveFilename(relativePath, parentModule) cache.registered = true cache.resourcePath = resourcePath cache.resourcePathWithTrailingSlash = "#{resourcePath}#{path.sep}" registerBuiltins(devMode) return exports.add = (directoryPath, metadata) -> # path.join isn't used in this function for speed since path.join calls # path.normalize and all the paths are already normalized here. unless metadata? try metadata = require("#{directoryPath}#{path.sep}package.json") catch error return cacheToAdd = metadata?._nylasModuleCache return unless cacheToAdd? for dependency in cacheToAdd.dependencies ? [] cache.dependencies[dependency.name] ?= {} cache.dependencies[dependency.name][dependency.version] ?= "#{directoryPath}#{path.sep}#{dependency.path}" for entry in cacheToAdd.folders ? [] for folderPath in entry.paths if folderPath cache.folders["#{directoryPath}#{path.sep}#{folderPath}"] = entry.dependencies else cache.folders[directoryPath] = entry.dependencies for extension, paths of cacheToAdd.extensions cache.extensions[extension] ?= new Set() for filePath in paths cache.extensions[extension].add("#{directoryPath}#{path.sep}#{filePath}") return exports.cache = cache