From b2153a33dd56ad4091b44c6bf3b340f3e4fa3c51 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Aug 2018 12:54:09 -0700 Subject: [PATCH] Decaffeinate ModuleCache --- src/module-cache.coffee | 285 --------------------------------- src/module-cache.js | 337 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+), 285 deletions(-) delete mode 100644 src/module-cache.coffee create mode 100644 src/module-cache.js diff --git a/src/module-cache.coffee b/src/module-cache.coffee deleted file mode 100644 index 358ed3393..000000000 --- a/src/module-cache.coffee +++ /dev/null @@ -1,285 +0,0 @@ -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 = null - -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 ['exports', '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 'atom' - 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 - - nativeModules ?= process.binding('natives') - 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[resolvedPath] or isCorePath(resolvedPath) - return resolvedPath if satisfies(version, range) - - return - -registerBuiltins = (devMode) -> - if devMode or not cache.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}") - fs = require 'fs-plus' - atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js') - cache.builtins.atom = atomJsPath if fs.isFileSync(atomJsPath) - cache.builtins.atom ?= path.join(cache.resourcePath, 'exports', 'atom.js') - - electronAsarRoot = path.join(process.resourcesPath, 'electron.asar') - - commonRoot = path.join(electronAsarRoot, 'common', 'api') - commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell'] - for builtin in commonBuiltins - cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js") - - rendererRoot = path.join(electronAsarRoot, 'renderer', 'api') - rendererBuiltins = ['ipc-renderer', 'remote', 'screen'] - for builtin in rendererBuiltins - cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") - -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._atomModuleCache = 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?._atomModuleCache - 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 diff --git a/src/module-cache.js b/src/module-cache.js new file mode 100644 index 000000000..a22072e4a --- /dev/null +++ b/src/module-cache.js @@ -0,0 +1,337 @@ +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