UNPKG

live-plugin-manager

Version:

Install and uninstall any node package at runtime from npm registry

457 lines 18.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VersionManager = exports.DefaultMainFile = void 0; const fs = __importStar(require("./fileSystem")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)("live-plugin-manager"); exports.DefaultMainFile = "index.js"; const cwd = process.cwd(); function createDefaultOptions() { return { cwd, rootPath: path.join(cwd, "plugin_packages", ".versions"), }; } /** * A class to manage the versions of the downloaded packages. */ class VersionManager { constructor(options) { if (options && !options.rootPath && options.cwd) { options.rootPath = path.join(options.cwd, "plugin_packages", ".versions"); } this.options = Object.assign(Object.assign({}, createDefaultOptions()), (options || {})); } /** * Ensure the root path exists. */ ensureRootPath() { return __awaiter(this, void 0, void 0, function* () { yield fs.ensureDir(this.options.rootPath); }); } /** * Get the location for the specified package name and version. * * @param packageInfo A package information to get the location * @returns A location for the specified package name and version */ getPath(packageInfo) { const { name, version } = packageInfo; return path.join(this.options.rootPath, `${name}@${version}`); } /** * Resolve the path for the specified package name and version. * * @param name A package name to resolve * @param version A package version to resolve * @returns */ resolvePath(name, version) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRootPath(); let searchPath = this.options.rootPath; let moduleName = name; if (name.includes("/")) { const index = name.lastIndexOf("/"); const scope = name.substring(0, index); searchPath = path.join(searchPath, scope); moduleName = name.substring(index + 1); if (!(yield fs.directoryExists(searchPath))) { return undefined; } } const files = yield fs.readdir(searchPath); const filename = files.find((f) => this.checkModuleFilenameSatisfied(f, moduleName, version)); if (filename === undefined) { return undefined; } return path.join(searchPath, filename); }); } /** * Download a package using a downloader. * Downloaded files are stored in the rootPath as directory named as `name@version`. * * @param downloader A downloader object that implements the download method * @param registryInfo A package info to download * @returns A information for the downloaded package */ download(downloader, registryInfo) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRootPath(); const destPath = this.options.rootPath; yield fs.ensureDir(destPath); const destPackagePath = yield downloader.download(destPath, registryInfo); const packageJson = yield this.readPackageJsonFromPath(destPackagePath); if (!packageJson) { throw new Error(`Invalid plugin ${destPackagePath}, package.json is missing`); } const versionPath = path.join(destPath, `${packageJson.name}@${packageJson.version}`); yield fs.rename(destPackagePath, versionPath); if (debug.enabled) { debug(`Downloaded package ${packageJson.name}@${packageJson.version} to ${versionPath}`); } const downloadedJson = yield this.readPackageJsonFromPath(versionPath); if (!downloadedJson) { throw new Error(`Invalid plugin ${versionPath}, package.json is missing`); } return downloadedJson; }); } /** * Uninstall packages which are not used by other packages. * * @param installedPlugins A list of the installed packages. * @returns A list of the uninstalled packages. */ uninstallOrphans(installedPlugins) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRootPath(); return yield this.uninstallOrphansLockFree(installedPlugins); }); } /** * Unload a version of a plugin if it is not used by any other plugin * * @param pluginInfo A plugin information to uninstall * @returns true if the version was unloaded, false if it was used by another plugin */ uninstallOrphan(pluginInfo) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRootPath(); const used = yield this.checkVersionUsedInDir(pluginInfo); if (used) { return false; } yield this.removeVersion(pluginInfo); return true; }); } /** * Create a plugin information for the specified version. * * @param name A package name * @param version A package version * @param withDependencies A flag to load dependency packages * @returns A plugin information for the specified version */ createVersionInfo(name, version, withDependencies = false) { return __awaiter(this, void 0, void 0, function* () { const location = path.join(this.options.rootPath, `${name}@${version}`); return yield this.createVersionInfoFromPath(location, withDependencies); }); } /** * Create a plugin information for the specified path. * * @param location A path to the package directory * @param withDependencies A flag to load dependency packages * @returns A plugin information for the specified path */ createVersionInfoFromPath(location, withDependencies = false) { return __awaiter(this, void 0, void 0, function* () { const packageJson = yield this.readPackageJsonFromPath(location); if (!packageJson) { throw new Error(`Invalid plugin ${location}, package.json is missing`); } const mainFile = path.normalize(path.join(location, packageJson.main || exports.DefaultMainFile)); if (!withDependencies) { return { name: packageJson.name, version: packageJson.version, location, mainFile, dependencies: packageJson.dependencies || {}, }; } const dependencies = packageJson.dependencies || {}; const dependencyNames = Object.keys(dependencies); const dependencyPackageJsons = yield Promise.all(dependencyNames.map((name) => __awaiter(this, void 0, void 0, function* () { const moduleLocation = path.join(location, "node_modules", name); return yield this.readPackageJsonFromPath(moduleLocation); }))); const dependencyDetails = {}; dependencyPackageJsons.forEach((p, i) => { dependencyDetails[dependencyNames[i]] = p; }); return { name: packageJson.name, version: packageJson.version, location, mainFile, dependencies, dependencyDetails, }; }); } /** * Check whether the filename is satisfied with the specified package name and version. * * @param filename A filename to check * @param name A package name to check * @param version A package version to check * @returns true if the filename is satisfied with the specified package name and version, otherwise false */ checkModuleFilenameSatisfied(filename, name, version) { const m = filename.match(/^(.+)@([^@]+)$/); if (!m) { return false; } if (m[1] !== name) { return false; } return semver.satisfies(m[2], version); } /** * Get the package information from the package directory. * * @param location A path to the package directory * @returns A package information for the package directory */ readPackageJsonFromPath(location) { return __awaiter(this, void 0, void 0, function* () { const packageJsonFile = path.join(location, "package.json"); if (!(yield fs.fileExists(packageJsonFile))) { return undefined; } const packageJson = JSON.parse(yield fs.readFile(packageJsonFile, "utf8")); if (!packageJson.name || !packageJson.version) { throw new Error(`Invalid plugin ${location}, 'main', 'name' and 'version' properties are required in package.json`); } return packageJson; }); } /** * List package directories in the specified base directory. * * @param baseDir A base directory to list * @param scope A scope for packages * @returns A list of the package directories */ listVersionDirs(baseDir, scope) { return __awaiter(this, void 0, void 0, function* () { const files = yield fs.readdir(baseDir); const versionDirs = []; for (const file of files) { if (file === "install.lock" || file === "node_modules") { continue; } const packageJsonPath = path.join(baseDir, file, "package.json"); if (yield fs.fileExists(packageJsonPath)) { versionDirs.push(scope ? `${scope}/${file}` : file); continue; } const subDir = path.join(baseDir, file); const subDirs = yield this.listVersionDirs(subDir, scope ? `${scope}/${file}` : file); versionDirs.push(...subDirs); } return versionDirs; }); } /** * Check whether the package is used by other packages. * * @param packageInfo A package information to check * @param baseDir A base directory to check. If not specified, the rootPath is used. * @returns true if the package is used by other packages, otherwise false */ checkVersionUsedInDir(packageInfo, baseDir) { return __awaiter(this, void 0, void 0, function* () { const { name, version } = packageInfo; const location = baseDir || this.options.rootPath; const files = yield this.listVersionDirs(location); if (debug.enabled) { debug(`Checking ${name}@${version} in ${location}`); } for (const file of files) { if (debug.enabled) { debug(`Checking ${name}@${version} in ${file}`); } const used = yield this.checkVersionUsedFromPackage(packageInfo, path.join(location, file)); if (used) { return true; } } return false; }); } /** * Check whether the package is used by the specified package. * * @param packageInfo A package information to check * @param packageDir A package directory to check * @returns true if the package is used by the specified package, otherwise false */ checkVersionUsedFromPackage(packageInfo, packageDir) { return __awaiter(this, void 0, void 0, function* () { let packageJson; try { packageJson = yield this.readPackageJsonFromPath(packageDir); } catch (e) { if (debug.enabled) { debug(`Cannot load package.json ${packageDir}`, e); } return false; } if (!packageJson) { return false; } if (!packageJson.dependencies) { return false; } const { name, version } = packageInfo; if (!packageJson.dependencies[name]) { return false; } if (!semver.validRange(packageJson.dependencies[name])) { if (debug.enabled) { debug(`Unexpected version range ${packageJson.dependencies[name]} for ${name}, treated as used.`); } return true; } if (semver.satisfies(version, packageJson.dependencies[name])) { if (debug.enabled) { debug(`Found ${name}@${version} in ${packageDir}`); } return true; } return false; }); } /** * Uninstall all of the orphaned packages. * * @param installedPlugins A list of the installed packages * @returns A list of the uninstalled packages */ uninstallOrphansLockFree(installedPlugins) { return __awaiter(this, void 0, void 0, function* () { const rootPath = this.options.rootPath; const files = yield this.listVersionDirs(rootPath); const orphans = []; if (debug.enabled) { debug(`Checking orphans in ${rootPath}`); } for (const file of files) { const fullPath = path.join(rootPath, file); if (file === "install.lock") { continue; } let packageJson; try { packageJson = yield this.readPackageJsonFromPath(fullPath); } catch (e) { if (debug.enabled) { debug(`Cannot load package.json ${fullPath}`, e); } continue; } if (!packageJson) { continue; } if (installedPlugins .find((p) => packageJson && p.name === packageJson.name && p.version === packageJson.version)) { continue; } let used = false; for (const anotherFile of files) { if (anotherFile === file) { continue; } if (yield this.checkVersionUsedFromPackage(packageJson, path.join(rootPath, anotherFile))) { used = true; break; } } if (used) { continue; } orphans.push(packageJson); } if (orphans.length === 0) { return []; } const uninstalled = []; for (const orphan of orphans) { const pluginInfo = yield this.createVersionInfo(orphan.name, orphan.version); yield this.removeVersion(pluginInfo); uninstalled.push(pluginInfo); } return uninstalled.concat(yield this.uninstallOrphansLockFree(installedPlugins)); }); } /** * Remove the specified version. * * @param pluginInfo A plugin information to remove */ removeVersion(pluginInfo) { return __awaiter(this, void 0, void 0, function* () { const pathSegments = pluginInfo.name.split("/"); pathSegments[pathSegments.length - 1] = `${pathSegments[pathSegments.length - 1]}@${pluginInfo.version}`; for (let i = 0; i < pathSegments.length; i++) { const pathToRemove = path.join(this.options.rootPath, ...pathSegments.slice(0, pathSegments.length - i)); if (debug.enabled) { debug(`Removing ${pathToRemove}`); } if (!(yield fs.directoryExists(pathToRemove))) { continue; } if (i > 0) { // For scoped packages, need to check if the parent directory is empty const files = yield fs.readdir(pathToRemove); if (files.length > 0) { if (debug.enabled) { debug(`Skip removing ${pathToRemove}, not empty`); } break; } } yield fs.remove(pathToRemove); } }); } } exports.VersionManager = VersionManager; //# sourceMappingURL=VersionManager.js.map