const fs = require('fs') const path = require('path') const LRU = require('lru-cache') const { chalk, semver } = require('@vue/cli-shared-utils') // Connectors const cwd = require('./cwd') const folders = require('./folders') const progress = require('./progress') const logs = require('./logs') // Context const getContext = require('../context') // Utils const { isPlugin, resolveModule } = require('@vue/cli-shared-utils') const { progress: installProgress } = require('@vue/cli/lib/util/executeCommand') const PackageManager = require('@vue/cli/lib/util/ProjectPackageManager') const { resolveModuleRoot } = require('../util/resolve-path') const { notify } = require('../util/notification') const { log } = require('../util/logger') const PROGRESS_ID = 'dependency-installation' const CLI_SERVICE = '@vue/cli-service' // Caches const metadataCache = new LRU({ max: 200, maxAge: 1000 * 60 * 30 // 30 min. }) // Local let dependencies function list (file, context) { const pkg = folders.readPackage(file, context) dependencies = [] dependencies = dependencies.concat( findDependencies(pkg.devDependencies || {}, 'devDependencies', file, context) ) dependencies = dependencies.concat( findDependencies(pkg.dependencies || {}, 'dependencies', file, context) ) return dependencies } function findOne (id, context) { return dependencies.find( p => p.id === id ) } function findDependencies (deps, type, file, context) { return Object.keys(deps).filter( id => !isPlugin(id) && id !== CLI_SERVICE ).map( id => ({ id, versionRange: deps[id], installed: isInstalled({ id, file }), website: getLink({ id, file }, context), type, baseFir: file }) ) } function getPath ({ id, file = cwd.get() }) { const filePath = resolveModule(path.join(id, 'package.json'), file) if (!filePath) return return resolveModuleRoot(filePath, id) } function isInstalled ({ id, file = cwd.get() }) { const resolvedPath = getPath({ id, file }) return resolvedPath && fs.existsSync(resolvedPath) } function readPackage ({ id, file }, context) { try { return folders.readPackage(getPath({ id, file }), context) } catch (e) { console.log(e) } return {} } function invalidatePackage ({ id, file }, context) { return folders.invalidatePackage(getPath({ id, file }), context) } async function getMetadata (id, context) { let metadata = metadataCache.get(id) if (metadata) { return metadata } try { metadata = await (new PackageManager({ context: cwd.get() })).getMetadata(id) } catch (e) { // No connection? } if (metadata) { metadataCache.set(id, metadata) return metadata } else { log('Dependencies', chalk.yellow('Can\'t load metadata'), id) } } async function getVersion ({ id, installed, versionRange, baseDir }, context) { let current // Is local dep const localPath = getLocalPath(id, context) // Read module package.json if (installed) { const pkg = readPackage({ id, file: baseDir }, context) current = pkg.version } else { current = null } // Metadata let latest, wanted const metadata = await getMetadata(id, context) if (metadata) { latest = metadata['dist-tags'].latest const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions) wanted = semver.maxSatisfying(versions, versionRange) } if (!latest) latest = current if (!wanted) wanted = current return { current, latest, wanted, range: versionRange, localPath } } function getLocalPath (id, context) { const projects = require('./projects') const projectPkg = folders.readPackage(projects.getCurrent(context).path, context, true) const deps = Object.assign( {}, projectPkg.dependencies || {}, projectPkg.devDependencies || {} ) const range = deps[id] if (range && range.match(/^file:/)) { const localPath = range.substr('file:'.length) return path.resolve(cwd.get(), localPath) } return null } async function getDescription ({ id }, context) { const metadata = await getMetadata(id, context) if (metadata) { return metadata.description } return null } function getLink ({ id, file }, context) { const pkg = readPackage({ id, file }, context) return pkg.homepage || (pkg.repository && pkg.repository.url) || `https://www.npmjs.com/package/${id.replace('/', '%2F')}` } function install ({ id, type, range }, context) { return progress.wrap(PROGRESS_ID, context, async setProgress => { setProgress({ status: 'dependency-install', args: [id] }) let arg if (range) { arg = `${id}@${range}` } else { arg = id } const pm = new PackageManager({ context: cwd.get() }) await pm.add(arg, { tilde: !range && isPlugin(id), dev: type === 'devDependencies' }) logs.add({ message: `Dependency ${id} installed`, type: 'info' }, context) notify({ title: 'Dependency installed', message: `Dependency ${id} successfully installed`, icon: 'done' }) list(cwd.get(), context) return findOne(id, context) }) } function uninstall ({ id }, context) { return progress.wrap(PROGRESS_ID, context, async setProgress => { setProgress({ status: 'dependency-uninstall', args: [id] }) const dep = findOne(id, context) const pm = new PackageManager({ context: cwd.get() }) await pm.remove(id) logs.add({ message: `Dependency ${id} uninstalled`, type: 'info' }, context) notify({ title: 'Dependency uninstalled', message: `Dependency ${id} successfully uninstalled`, icon: 'done' }) return dep }) } function update ({ id }, context) { return progress.wrap(PROGRESS_ID, context, async setProgress => { setProgress({ status: 'dependency-update', args: [id] }) const dep = findOne(id, context) const { current, wanted } = await getVersion(dep, context) const pm = new PackageManager({ context: cwd.get() }) await pm.upgrade(id) logs.add({ message: `Dependency ${id} updated from ${current} to ${wanted}`, type: 'info' }, context) notify({ title: 'Dependency updated', message: `Dependency ${id} was successfully updated`, icon: 'done' }) invalidatePackage({ id }, context) return findOne(id) }) } function updateAll (context) { return progress.wrap(PROGRESS_ID, context, async setProgress => { const deps = list(cwd.get(), context) const updatedDeps = [] for (const dep of deps) { const version = await getVersion(dep, context) if (version.current !== version.wanted) { updatedDeps.push(dep) invalidatePackage({ id: dep.id }, context) } } if (!updatedDeps.length) { notify({ title: 'No updates available', message: 'No dependency to update in the version ranges declared in package.json', icon: 'done' }) return [] } setProgress({ status: 'dependencies-update', args: [updatedDeps.length] }) const pm = new PackageManager({ context: cwd.get() }) await pm.upgrade(updatedDeps.map(p => p.id).join(' ')) notify({ title: 'Dependencies updated', message: `${updatedDeps.length} dependencies were successfully updated`, icon: 'done' }) return updatedDeps }) } function setup (context) { // Package installation progress events installProgress.on('progress', value => { if (progress.get(PROGRESS_ID)) { progress.set({ id: PROGRESS_ID, progress: value }, context) } }) installProgress.on('log', message => { if (progress.get(PROGRESS_ID)) { progress.set({ id: PROGRESS_ID, info: message }, context) } }) } setup(getContext()) module.exports = { list, findOne, getPath, getMetadata, getLink, getDescription, getVersion, install, uninstall, update, updateAll, invalidatePackage }