const Module = require('module') const path = require('path') const semver = require('semver') // Extend semver.Range to memoize matched versions for speed class Range extends semver.Range { constructor () { super(...arguments) this.matchedVersions = new Set() this.unmatchedVersions = new Set() } test (version) { if (this.matchedVersions.has(version)) return true if (this.unmatchedVersions.has(version)) return false const matches = super.test(...arguments) if (matches) { this.matchedVersions.add(version) } else { this.unmatchedVersions.add(version) } return matches } } let nativeModules = null const 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. let isAbsolute if (process.platform === 'win32') { isAbsolute = pathToCheck => pathToCheck && ((pathToCheck[1] === ':') || ((pathToCheck[0] === '\\') && (pathToCheck[1] === '\\'))) } else { isAbsolute = pathToCheck => pathToCheck && (pathToCheck[0] === '/') } const isCorePath = pathToCheck => pathToCheck.startsWith(cache.resourcePathWithTrailingSlash) function loadDependencies (modulePath, rootPath, rootMetadata, moduleCache) { const fs = require('fs-plus') for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { if (path.basename(childPath) === '.bin') continue if (rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(path.basename(childPath)))) { continue } const childMetadataPath = path.join(childPath, 'package.json') if (!fs.isFileSync(childMetadataPath)) continue const childMetadata = JSON.parse(fs.readFileSync(childMetadataPath)) if (childMetadata && childMetadata.version) { var mainPath 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) } } } function loadFolderCompatibility (modulePath, rootPath, rootMetadata, moduleCache) { const fs = require('fs-plus') const metadataPath = path.join(modulePath, 'package.json') if (!fs.isFileSync(metadataPath)) return const metadata = JSON.parse(fs.readFileSync(metadataPath)) const dependencies = metadata.dependencies || {} for (let name in dependencies) { if (!semver.validRange(dependencies[name])) { delete dependencies[name] } } const onDirectory = childPath => path.basename(childPath) !== 'node_modules' const extensions = ['.js', '.coffee', '.json', '.node'] let paths = {} function onFile (childPath) { const needle = path.extname(childPath) if (extensions.includes(needle)) { const relativePath = path.relative(rootPath, path.dirname(childPath)) paths[relativePath] = true } } fs.traverseTreeSync(modulePath, onFile, onDirectory) paths = Object.keys(paths) if (paths.length > 0 && Object.keys(dependencies).length > 0) { moduleCache.folders.push({paths, dependencies}) } for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { if (path.basename(childPath) === '.bin') continue if (rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(path.basename(childPath)))) { continue } loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache) } } function loadExtensions (modulePath, rootPath, rootMetadata, moduleCache) { const fs = require('fs-plus') const extensions = ['.js', '.coffee', '.json', '.node'] const nodeModulesPath = path.join(rootPath, 'node_modules') function onFile (filePath) { filePath = path.relative(rootPath, filePath) const segments = filePath.split(path.sep) if (segments.includes('test')) return if (segments.includes('tests')) return if (segments.includes('spec')) return if (segments.includes('specs')) return if (segments.length > 1 && !['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'].includes(segments[0])) return const extension = path.extname(filePath) if (extensions.includes(extension)) { if (moduleCache.extensions[extension] == null) { moduleCache.extensions[extension] = [] } moduleCache.extensions[extension].push(filePath) } } function onDirectory (childPath) { // Don't include extensions from bundled packages // These are generated and stored in the package's own metadata cache if (rootMetadata.name === 'atom') { const parentPath = path.dirname(childPath) if (parentPath === nodeModulesPath) { const packageName = path.basename(childPath) if (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(packageName)) return false } } return true } fs.traverseTreeSync(rootPath, onFile, onDirectory) } function satisfies (version, rawRange) { let parsedRange if (!(parsedRange = cache.ranges[rawRange])) { parsedRange = new Range(rawRange) cache.ranges[rawRange] = parsedRange } return parsedRange.test(version) } function resolveFilePath (relativePath, parentModule) { if (!relativePath) return if (!(parentModule && parentModule.filename)) return if (relativePath[0] !== '.' && !isAbsolute(relativePath)) return const resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath) if (!isCorePath(resolvedPath)) return let extension = path.extname(resolvedPath) if (extension) { if (cache.extensions[extension] && cache.extensions[extension].has(resolvedPath)) return resolvedPath } else { for (extension in cache.extensions) { const paths = cache.extensions[extension] const resolvedPathWithExtension = `${resolvedPath}${extension}` if (paths.has(resolvedPathWithExtension)) { return resolvedPathWithExtension } } } } function resolveModulePath (relativePath, parentModule) { if (!relativePath) return if (!(parentModule && parentModule.filename)) return if (!nativeModules) nativeModules = process.binding('natives') if (nativeModules.hasOwnProperty(relativePath)) return if (relativePath[0] === '.') return if (isAbsolute(relativePath)) return const folderPath = path.dirname(parentModule.filename) const range = cache.folders[folderPath] && cache.folders[folderPath][relativePath] if (!range) { const builtinPath = cache.builtins[relativePath] if (builtinPath) { return builtinPath } else { return } } const candidates = cache.dependencies[relativePath] if (candidates == null) return for (let version in candidates) { const resolvedPath = candidates[version] if (Module._cache[resolvedPath] || isCorePath(resolvedPath)) { if (satisfies(version, range)) return resolvedPath } } } function registerBuiltins (devMode) { if (devMode || !cache.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`)) { const fs = require('fs-plus') const atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js') if (fs.isFileSync(atomJsPath)) { cache.builtins.atom = atomJsPath } } if (cache.builtins.atom == null) { cache.builtins.atom = path.join(cache.resourcePath, 'exports', 'atom.js') } const electronAsarRoot = path.join(process.resourcesPath, 'electron.asar') const commonRoot = path.join(electronAsarRoot, 'common', 'api') const commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell'] for (const builtin of commonBuiltins) { cache.builtins[builtin] = path.join(commonRoot, `${builtin}.js`) } const rendererRoot = path.join(electronAsarRoot, 'renderer', 'api') const rendererBuiltins = ['ipc-renderer', 'remote', 'screen'] for (const builtin of rendererBuiltins) { cache.builtins[builtin] = path.join(rendererRoot, `${builtin}.js`) } } exports.create = function (modulePath) { const fs = require('fs-plus') modulePath = fs.realpathSync(modulePath) const metadataPath = path.join(modulePath, 'package.json') const metadata = JSON.parse(fs.readFileSync(metadataPath)) const moduleCache = { version: 1, dependencies: [], extensions: {}, folders: [] } loadDependencies(modulePath, modulePath, metadata, moduleCache) loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache) loadExtensions(modulePath, modulePath, metadata, moduleCache) metadata._atomModuleCache = moduleCache fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)) } exports.register = function ({resourcePath, devMode} = {}) { if (cache.registered) return const originalResolveFilename = Module._resolveFilename Module._resolveFilename = function (relativePath, parentModule) { let resolvedPath = resolveModulePath(relativePath, parentModule) if (!resolvedPath) { resolvedPath = resolveFilePath(relativePath, parentModule) } return resolvedPath || originalResolveFilename(relativePath, parentModule) } cache.registered = true cache.resourcePath = resourcePath cache.resourcePathWithTrailingSlash = `${resourcePath}${path.sep}` registerBuiltins(devMode) } exports.add = function (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. if (metadata == null) { try { metadata = require(`${directoryPath}${path.sep}package.json`) } catch (error) { return } } const cacheToAdd = metadata && metadata._atomModuleCache if (!cacheToAdd) return for (const dependency of cacheToAdd.dependencies || []) { if (!cache.dependencies[dependency.name]) { cache.dependencies[dependency.name] = {} } if (!cache.dependencies[dependency.name][dependency.version]) { cache.dependencies[dependency.name][dependency.version] = `${directoryPath}${path.sep}${dependency.path}` } } for (const entry of cacheToAdd.folders || []) { for (const folderPath of entry.paths) { if (folderPath) { cache.folders[`${directoryPath}${path.sep}${folderPath}`] = entry.dependencies } else { cache.folders[directoryPath] = entry.dependencies } } } for (const extension in cacheToAdd.extensions) { const paths = cacheToAdd.extensions[extension] if (!cache.extensions[extension]) { cache.extensions[extension] = new Set() } for (let filePath of paths) { cache.extensions[extension].add(`${directoryPath}${path.sep}${filePath}`) } } } exports.cache = cache exports.Range = Range