Files
atom/src/module-cache.js
2018-08-24 14:22:27 -07:00

340 lines
11 KiB
JavaScript

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