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 = ['clipboard', 'shell']; for (const builtin of commonBuiltins) { cache.builtins[builtin] = path.join(commonRoot, `${builtin}.js`); } const rendererRoot = path.join(electronAsarRoot, 'renderer', 'api'); const rendererBuiltins = [ 'crash-reporter', '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;