Files
atom/src/module-cache.js
2019-05-31 18:33:56 +02:00

396 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;