Skip to content

Commit

Permalink
[cli] Replace update-notifier with custom, non-lazy-loaded version
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars authored and bjoerge committed Feb 16, 2018
1 parent c26fc06 commit c7d4ecf
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 26 deletions.
4 changes: 3 additions & 1 deletion packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@sanity/client": "^0.125.8",
"@sanity/resolver": "^0.125.8",
"@sanity/util": "^0.125.8",
"boxen": "^1.3.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.1.0",
Expand All @@ -53,6 +54,7 @@
"git-user-info": "^1.0.1",
"gitconfiglocal": "^2.0.1",
"inquirer": "^2.0.0",
"is-installed-globally": "^0.1.0",
"klaw-sync": "^3.0.2",
"latest-version": "^3.1.0",
"leven": "^2.1.0",
Expand All @@ -72,9 +74,9 @@
"simple-get": "^2.7.0",
"split2": "^2.1.1",
"thenify": "^3.3.0",
"update-notifier": "^1.0.3",
"validate-npm-package-name": "^3.0.0",
"webpack": "^3.8.1",
"which": "^1.3.0",
"xdg-basedir": "^3.0.0"
}
}
73 changes: 48 additions & 25 deletions packages/@sanity/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import path from 'path'
import chalk from 'chalk'
import fse from 'fs-extra'
import resolveFrom from 'resolve-from'
import updateNotifier from 'update-notifier'
import {resolveProjectRoot} from '@sanity/resolver'
import pkg from '../package.json'
import updateNotifier from './util/updateNotifier'
import parseArguments from './util/parseArguments'
import mergeCommands from './util/mergeCommands'
import {getCliRunner} from './CommandRunner'
Expand All @@ -15,34 +15,25 @@ import baseCommands from './commands'
const sanityEnv = process.env.SANITY_ENV || 'production' // eslint-disable-line no-process-env
const knownEnvs = ['development', 'staging', 'production']

module.exports = function runCli(cliRoot) {
module.exports = async function runCli(cliRoot) {
installUnhandledRejectionsHandler()
updateNotifier({pkg}).notify({defer: false})

const args = parseArguments()
const isInit = args.groupOrCommand === 'init' && args.argsWithoutOptions[0] !== 'plugin'
const isInit =
args.groupOrCommand === 'init' && args.argsWithoutOptions[0] !== 'plugin'
const cwd = checkCwdPresence()
const workDir = isInit ? process.cwd() : resolveRootDir(cwd)

await updateNotifier({pkg, cwd, workDir}).notify()

const options = {
cliRoot: cliRoot,
workDir: workDir,
corePath: getCoreModulePath(workDir)
}

if (sanityEnv !== 'production') {
console.warn(
chalk.yellow(
knownEnvs.includes(sanityEnv)
? `[WARN] Running in ${sanityEnv} environment mode\n`
: `[WARN] Running in ${chalk.red('UNKNOWN')} "${sanityEnv}" environment mode\n`
)
)
}

if (!isInit && workDir !== cwd) {
console.log(`Not in project directory, assuming context of project at ${workDir}`)
}
warnOnNonProductionEnvironment()
warnOnInferredProjectDir(isInit, cwd, workDir)

const core = args.coreOptions
const commands = mergeCommands(baseCommands, options.corePath)
Expand Down Expand Up @@ -71,12 +62,6 @@ module.exports = function runCli(cliRoot) {
})
}

function installUnhandledRejectionsHandler() {
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason.stack)
})
}

function getCoreModulePath(workDir) {
const pkgPath = resolveFrom.silent(workDir, '@sanity/core')
if (pkgPath) {
Expand Down Expand Up @@ -105,7 +90,9 @@ function checkCwdPresence() {
pwd = process.cwd()
} catch (err) {
if (err.code === 'ENOENT') {
console.error('[ERR] Could not resolve working directory, does the current folder exist?')
console.error(
'[ERR] Could not resolve working directory, does the current folder exist?'
)
process.exit(1)
} else {
throw err
Expand All @@ -126,10 +113,46 @@ function resolveRootDir(cwd) {
)
} catch (err) {
console.warn(
chalk.red(['Error occured trying to resolve project root:', err.message].join('\n'))
chalk.red(
['Error occured trying to resolve project root:', err.message].join(
'\n'
)
)
)
process.exit(1)
}

return false
}

function installUnhandledRejectionsHandler() {
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason.stack)
})
}

function warnOnInferredProjectDir(isInit, cwd, workDir) {
if (isInit || cwd === workDir) {
return
}

console.log(
`Not in project directory, assuming context of project at ${workDir}`
)
}

function warnOnNonProductionEnvironment() {
if (sanityEnv === 'production') {
return
}

console.warn(
chalk.yellow(
knownEnvs.includes(sanityEnv)
? `[WARN] Running in ${sanityEnv} environment mode\n`
: `[WARN] Running in ${chalk.red(
'UNKNOWN'
)} "${sanityEnv}" environment mode\n`
)
)
}
174 changes: 174 additions & 0 deletions packages/@sanity/cli/src/util/updateNotifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* eslint-disable no-process-env */
import path from 'path'
import which from 'which'
import boxen from 'boxen'
import chalk from 'chalk'
import latestVersion from 'latest-version'
import semverCompare from 'semver-compare'
import isInstalledGlobally from 'is-installed-globally'
import debug from '../debug'
import getUserConfig from './getUserConfig'

const MAX_BLOCKING_TIME = 250
const TWELVE_HOURS = 1000 * 60 * 60 * 12
const isDisabled =
process.env.CI || // Travis CI, CircleCI, Gitlab CI, Appveyor, CodeShip
process.env.CONTINUOUS_INTEGRATION || // Travis CI
process.env.BUILD_NUMBER || // Jenkins, TeamCity
process.env.NO_UPDATE_NOTIFIER // Explicitly disabled

module.exports = options => {
debug('CLI installed at %s', __dirname)

const {pkg, cwd, workDir} = options
const {name, version} = pkg
const userConfig = getUserConfig()
const check = getLatestRemote().catch(() => false)
let hasPrintedResult = false

return {notify}

async function notify() {
if (!process.stdout.isTTY) {
return
}

const maxWait = setTimeout(printCachedResult, MAX_BLOCKING_TIME)
const result = await check
if (hasPrintedResult) {
return
}

clearTimeout(maxWait)
printResult(result)
}

function printCachedResult() {
debug('Max time reached waiting for latest version info')
hasPrintedResult = true

const cached = userConfig.get('cliHasUpdate')
if (!cached) {
debug('No cached latest version result found')
return
}

const diff = semverCompare(cached, version)
if (diff <= 0) {
// Looks like CLI was upgraded since last check
debug('CLI was upgraded since last check, falling back')
userConfig.delete('cliHasUpdate')
return
}

debug('Printing cached latest version result')
printResult(cached)
}

function printResult(newVersion) {
hasPrintedResult = true

const lastUpdated = userConfig.get('cliLastUpdateNag') || 0
if (Date.now() - lastUpdated < TWELVE_HOURS) {
debug('Less than 12 hours since last nag, skipping')
return
}

if (!newVersion) {
return
}

const upgradeCommand = getUpgradeCommand(newVersion)
const message = [
'Update available ',
chalk.dim(version),
chalk.reset(' → '),
chalk.green(newVersion),
' \nRun ',
chalk.cyan(upgradeCommand),
' to update'
].join('')

const boxenOpts = {
padding: 1,
margin: 1,
align: 'center',
borderColor: 'yellow',
borderStyle: 'round'
}

// Print to stderr to prevent garbling command output
// eslint-disable-next-line no-console
console.error(`\n${boxen(message, boxenOpts)}`)

userConfig.set('cliLastUpdateNag', Date.now())
}

function getUpgradeCommand(newVersion) {
if (isInstalledGlobally && isInstalledUsingYarn()) {
debug('CLI is installed globally with yarn')
return `yarn global add ${name}`
}

if (isInstalledGlobally) {
debug('CLI is installed globally with npm')
return `npm install -g ${name}`
}

const cmds = cwd === workDir ? [] : [`cd ${path.relative(cwd, workDir)}`]
const hasGlobalYarn = Boolean(which.sync('yarn', {nothrow: true}))
if (hasGlobalYarn) {
cmds.push(`yarn upgrade ${name}`)
} else {
cmds.push('./node_modules/.bin/sanity upgrade @sanity/cli')
}

return cmds.join(' && ')
}

async function getLatestRemote() {
if (isDisabled) {
debug('Running on CI, or explicitly disabled, skipping update check')
return false
}

const lastUpdated = userConfig.get('cliLastUpdateCheck') || 0
if (Date.now() - lastUpdated < TWELVE_HOURS) {
debug('Less than 12 hours since last check, skipping update check')
return userConfig.get('cliHasUpdate') || false
}

let latestRemote
try {
latestRemote = await latestVersion(name)
debug('Latest remote version is %s', latestRemote)
} catch (err) {
debug(`Failed to fetch latest version of ${name} from npm:\n${err.stack}`)
return false
}

userConfig.set('cliLastUpdateCheck', Date.now())

const diff = semverCompare(latestRemote, version)
if (diff <= 0) {
// No change, or lower
debug(diff === 0 ? 'No update found' : 'Remote version older than local')
userConfig.delete('cliHasUpdate')
return false
}

// Update available, set to user config so we may notify on next startup
userConfig.set('cliHasUpdate', latestRemote)
debug('Update is available (%s)', latestRemote)
return latestRemote
}
}

function isInstalledUsingYarn() {
const isWindows = process.platform === 'win32'
const yarnPath = isWindows
? path.join('Yarn', 'config', 'global')
: path.join('.config', 'yarn', 'global')

return __dirname.includes(yarnPath)
}

0 comments on commit c7d4ecf

Please sign in to comment.