module-cache.js
1 const Module = require('module'); 2 const path = require('path'); 3 const semver = require('semver'); 4 5 // Extend semver.Range to memoize matched versions for speed 6 class Range extends semver.Range { 7 constructor() { 8 super(...arguments); 9 this.matchedVersions = new Set(); 10 this.unmatchedVersions = new Set(); 11 } 12 13 test(version) { 14 if (this.matchedVersions.has(version)) return true; 15 if (this.unmatchedVersions.has(version)) return false; 16 17 const matches = super.test(...arguments); 18 if (matches) { 19 this.matchedVersions.add(version); 20 } else { 21 this.unmatchedVersions.add(version); 22 } 23 return matches; 24 } 25 } 26 27 let nativeModules = null; 28 29 const cache = { 30 builtins: {}, 31 debug: false, 32 dependencies: {}, 33 extensions: {}, 34 folders: {}, 35 ranges: {}, 36 registered: false, 37 resourcePath: null, 38 resourcePathWithTrailingSlash: null 39 }; 40 41 // isAbsolute is inlined from fs-plus so that fs-plus itself can be required 42 // from this cache. 43 let isAbsolute; 44 if (process.platform === 'win32') { 45 isAbsolute = pathToCheck => 46 pathToCheck && 47 (pathToCheck[1] === ':' || 48 (pathToCheck[0] === '\\' && pathToCheck[1] === '\\')); 49 } else { 50 isAbsolute = pathToCheck => pathToCheck && pathToCheck[0] === '/'; 51 } 52 53 const isCorePath = pathToCheck => 54 pathToCheck.startsWith(cache.resourcePathWithTrailingSlash); 55 56 function loadDependencies(modulePath, rootPath, rootMetadata, moduleCache) { 57 const fs = require('fs-plus'); 58 59 for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { 60 if (path.basename(childPath) === '.bin') continue; 61 if ( 62 rootPath === modulePath && 63 (rootMetadata.packageDependencies && 64 rootMetadata.packageDependencies.hasOwnProperty( 65 path.basename(childPath) 66 )) 67 ) { 68 continue; 69 } 70 71 const childMetadataPath = path.join(childPath, 'package.json'); 72 if (!fs.isFileSync(childMetadataPath)) continue; 73 74 const childMetadata = JSON.parse(fs.readFileSync(childMetadataPath)); 75 if (childMetadata && childMetadata.version) { 76 var mainPath; 77 try { 78 mainPath = require.resolve(childPath); 79 } catch (error) { 80 mainPath = null; 81 } 82 83 if (mainPath) { 84 moduleCache.dependencies.push({ 85 name: childMetadata.name, 86 version: childMetadata.version, 87 path: path.relative(rootPath, mainPath) 88 }); 89 } 90 91 loadDependencies(childPath, rootPath, rootMetadata, moduleCache); 92 } 93 } 94 } 95 96 function loadFolderCompatibility( 97 modulePath, 98 rootPath, 99 rootMetadata, 100 moduleCache 101 ) { 102 const fs = require('fs-plus'); 103 104 const metadataPath = path.join(modulePath, 'package.json'); 105 if (!fs.isFileSync(metadataPath)) return; 106 107 const metadata = JSON.parse(fs.readFileSync(metadataPath)); 108 const dependencies = metadata.dependencies || {}; 109 110 for (let name in dependencies) { 111 if (!semver.validRange(dependencies[name])) { 112 delete dependencies[name]; 113 } 114 } 115 116 const onDirectory = childPath => path.basename(childPath) !== 'node_modules'; 117 118 const extensions = ['.js', '.coffee', '.json', '.node']; 119 let paths = {}; 120 function onFile(childPath) { 121 const needle = path.extname(childPath); 122 if (extensions.includes(needle)) { 123 const relativePath = path.relative(rootPath, path.dirname(childPath)); 124 paths[relativePath] = true; 125 } 126 } 127 fs.traverseTreeSync(modulePath, onFile, onDirectory); 128 129 paths = Object.keys(paths); 130 if (paths.length > 0 && Object.keys(dependencies).length > 0) { 131 moduleCache.folders.push({ paths, dependencies }); 132 } 133 134 for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { 135 if (path.basename(childPath) === '.bin') continue; 136 if ( 137 rootPath === modulePath && 138 (rootMetadata.packageDependencies && 139 rootMetadata.packageDependencies.hasOwnProperty( 140 path.basename(childPath) 141 )) 142 ) { 143 continue; 144 } 145 loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache); 146 } 147 } 148 149 function loadExtensions(modulePath, rootPath, rootMetadata, moduleCache) { 150 const fs = require('fs-plus'); 151 const extensions = ['.js', '.coffee', '.json', '.node']; 152 const nodeModulesPath = path.join(rootPath, 'node_modules'); 153 154 function onFile(filePath) { 155 filePath = path.relative(rootPath, filePath); 156 const segments = filePath.split(path.sep); 157 if (segments.includes('test')) return; 158 if (segments.includes('tests')) return; 159 if (segments.includes('spec')) return; 160 if (segments.includes('specs')) return; 161 if ( 162 segments.length > 1 && 163 !['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'].includes( 164 segments[0] 165 ) 166 ) 167 return; 168 169 const extension = path.extname(filePath); 170 if (extensions.includes(extension)) { 171 if (moduleCache.extensions[extension] == null) { 172 moduleCache.extensions[extension] = []; 173 } 174 moduleCache.extensions[extension].push(filePath); 175 } 176 } 177 178 function onDirectory(childPath) { 179 // Don't include extensions from bundled packages 180 // These are generated and stored in the package's own metadata cache 181 if (rootMetadata.name === 'atom') { 182 const parentPath = path.dirname(childPath); 183 if (parentPath === nodeModulesPath) { 184 const packageName = path.basename(childPath); 185 if ( 186 rootMetadata.packageDependencies && 187 rootMetadata.packageDependencies.hasOwnProperty(packageName) 188 ) 189 return false; 190 } 191 } 192 193 return true; 194 } 195 196 fs.traverseTreeSync(rootPath, onFile, onDirectory); 197 } 198 199 function satisfies(version, rawRange) { 200 let parsedRange; 201 if (!(parsedRange = cache.ranges[rawRange])) { 202 parsedRange = new Range(rawRange); 203 cache.ranges[rawRange] = parsedRange; 204 } 205 return parsedRange.test(version); 206 } 207 208 function resolveFilePath(relativePath, parentModule) { 209 if (!relativePath) return; 210 if (!(parentModule && parentModule.filename)) return; 211 if (relativePath[0] !== '.' && !isAbsolute(relativePath)) return; 212 213 const resolvedPath = path.resolve( 214 path.dirname(parentModule.filename), 215 relativePath 216 ); 217 if (!isCorePath(resolvedPath)) return; 218 219 let extension = path.extname(resolvedPath); 220 if (extension) { 221 if ( 222 cache.extensions[extension] && 223 cache.extensions[extension].has(resolvedPath) 224 ) 225 return resolvedPath; 226 } else { 227 for (extension in cache.extensions) { 228 const paths = cache.extensions[extension]; 229 const resolvedPathWithExtension = `${resolvedPath}${extension}`; 230 if (paths.has(resolvedPathWithExtension)) { 231 return resolvedPathWithExtension; 232 } 233 } 234 } 235 } 236 237 function resolveModulePath(relativePath, parentModule) { 238 if (!relativePath) return; 239 if (!(parentModule && parentModule.filename)) return; 240 241 if (!nativeModules) nativeModules = process.binding('natives'); 242 if (nativeModules.hasOwnProperty(relativePath)) return; 243 if (relativePath[0] === '.') return; 244 if (isAbsolute(relativePath)) return; 245 246 const folderPath = path.dirname(parentModule.filename); 247 248 const range = 249 cache.folders[folderPath] && cache.folders[folderPath][relativePath]; 250 if (!range) { 251 const builtinPath = cache.builtins[relativePath]; 252 if (builtinPath) { 253 return builtinPath; 254 } else { 255 return; 256 } 257 } 258 259 const candidates = cache.dependencies[relativePath]; 260 if (candidates == null) return; 261 262 for (let version in candidates) { 263 const resolvedPath = candidates[version]; 264 if (Module._cache[resolvedPath] || isCorePath(resolvedPath)) { 265 if (satisfies(version, range)) return resolvedPath; 266 } 267 } 268 } 269 270 function registerBuiltins(devMode) { 271 if ( 272 devMode || 273 !cache.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`) 274 ) { 275 const fs = require('fs-plus'); 276 const atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js'); 277 if (fs.isFileSync(atomJsPath)) { 278 cache.builtins.atom = atomJsPath; 279 } 280 } 281 if (cache.builtins.atom == null) { 282 cache.builtins.atom = path.join(cache.resourcePath, 'exports', 'atom.js'); 283 } 284 285 const electronAsarRoot = path.join(process.resourcesPath, 'electron.asar'); 286 287 const commonRoot = path.join(electronAsarRoot, 'common', 'api'); 288 const commonBuiltins = ['clipboard', 'shell']; 289 for (const builtin of commonBuiltins) { 290 cache.builtins[builtin] = path.join(commonRoot, `${builtin}.js`); 291 } 292 293 const rendererRoot = path.join(electronAsarRoot, 'renderer', 'api'); 294 const rendererBuiltins = [ 295 'crash-reporter', 296 'ipc-renderer', 297 'remote', 298 'screen' 299 ]; 300 for (const builtin of rendererBuiltins) { 301 cache.builtins[builtin] = path.join(rendererRoot, `${builtin}.js`); 302 } 303 } 304 305 exports.create = function(modulePath) { 306 const fs = require('fs-plus'); 307 308 modulePath = fs.realpathSync(modulePath); 309 const metadataPath = path.join(modulePath, 'package.json'); 310 const metadata = JSON.parse(fs.readFileSync(metadataPath)); 311 312 const moduleCache = { 313 version: 1, 314 dependencies: [], 315 extensions: {}, 316 folders: [] 317 }; 318 319 loadDependencies(modulePath, modulePath, metadata, moduleCache); 320 loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache); 321 loadExtensions(modulePath, modulePath, metadata, moduleCache); 322 323 metadata._atomModuleCache = moduleCache; 324 fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); 325 }; 326 327 exports.register = function({ resourcePath, devMode } = {}) { 328 if (cache.registered) return; 329 330 const originalResolveFilename = Module._resolveFilename; 331 Module._resolveFilename = function(relativePath, parentModule) { 332 let resolvedPath = resolveModulePath(relativePath, parentModule); 333 if (!resolvedPath) { 334 resolvedPath = resolveFilePath(relativePath, parentModule); 335 } 336 return resolvedPath || originalResolveFilename(relativePath, parentModule); 337 }; 338 339 cache.registered = true; 340 cache.resourcePath = resourcePath; 341 cache.resourcePathWithTrailingSlash = `${resourcePath}${path.sep}`; 342 registerBuiltins(devMode); 343 }; 344 345 exports.add = function(directoryPath, metadata) { 346 // path.join isn't used in this function for speed since path.join calls 347 // path.normalize and all the paths are already normalized here. 348 349 if (metadata == null) { 350 try { 351 metadata = require(`${directoryPath}${path.sep}package.json`); 352 } catch (error) { 353 return; 354 } 355 } 356 357 const cacheToAdd = metadata && metadata._atomModuleCache; 358 if (!cacheToAdd) return; 359 360 for (const dependency of cacheToAdd.dependencies || []) { 361 if (!cache.dependencies[dependency.name]) { 362 cache.dependencies[dependency.name] = {}; 363 } 364 if (!cache.dependencies[dependency.name][dependency.version]) { 365 cache.dependencies[dependency.name][ 366 dependency.version 367 ] = `${directoryPath}${path.sep}${dependency.path}`; 368 } 369 } 370 371 for (const entry of cacheToAdd.folders || []) { 372 for (const folderPath of entry.paths) { 373 if (folderPath) { 374 cache.folders[`${directoryPath}${path.sep}${folderPath}`] = 375 entry.dependencies; 376 } else { 377 cache.folders[directoryPath] = entry.dependencies; 378 } 379 } 380 } 381 382 for (const extension in cacheToAdd.extensions) { 383 const paths = cacheToAdd.extensions[extension]; 384 if (!cache.extensions[extension]) { 385 cache.extensions[extension] = new Set(); 386 } 387 for (let filePath of paths) { 388 cache.extensions[extension].add(`${directoryPath}${path.sep}${filePath}`); 389 } 390 } 391 }; 392 393 exports.cache = cache; 394 395 exports.Range = Range;