-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[cli] Replace update-notifier
dependency with built in
#8090
Changes from 44 commits
c38e118
f8b06e0
01fd56d
d36c1ec
03d0fdb
81be569
c46800d
c87c22a
0d72cb2
21494cb
d602459
9d5660c
6a48252
11c4129
283d91d
9981b47
cbba870
eb3713e
c10679d
7edf3b4
b8cbb20
469cf96
0597798
e952a1f
5b7fce5
21957af
055b908
5a021cb
a652572
250c83d
87c6399
1225467
035aea4
9ef6d5d
c2ad59b
78a79e1
72835e8
118cd7b
d7a122c
2735cb5
3bb5005
1633be7
d1c15d5
e46eadb
da2d962
7dbeda7
21b5106
fb9ae3c
14edff0
4cd39b0
abc40e3
8caddf5
d23ef97
194d888
cd50dfa
a30df0e
975d0a7
35f0f8c
53490b7
8f8508d
d41e739
4287e2d
99c82c0
286dd6a
6d45a3f
c91e5da
1a768cc
2a3540f
de4b687
205b6c8
a3cb3b0
c08c50c
7d040cb
def2926
226a232
5523b07
c765778
e1297f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import cpy from 'cpy'; | ||
import execa from 'execa'; | ||
import { join } from 'path'; | ||
import { remove, writeFile } from 'fs-extra'; | ||
import { remove, readJSON, writeFile } from 'fs-extra'; | ||
|
||
const dirRoot = join(__dirname, '..'); | ||
const distRoot = join(dirRoot, 'dist'); | ||
|
@@ -43,15 +43,15 @@ async function main() { | |
stdio: 'inherit', | ||
}); | ||
|
||
const pkg = await readJSON(join(dirRoot, 'package.json')); | ||
const dependencies = Object.keys(pkg?.dependencies ?? {}); | ||
cb1kenobi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Do the initial `ncc` build | ||
console.log(); | ||
const args = [ | ||
'ncc', | ||
'build', | ||
'--external', | ||
'update-notifier', | ||
'src/index.ts', | ||
]; | ||
console.log('Dependencies:', dependencies); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking - what if dependencies is empty? Should we still log? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we update this in a follow-up PR? We probably don't want to log if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On one hand, this is a build script and if there are no dependencies, then I'd think we'd want to see that. On the other hand, |
||
const externs = []; | ||
for (const dep of dependencies) { | ||
externs.push('--external', dep); | ||
} | ||
const args = ['ncc', 'build', 'src/index.ts', ...externs]; | ||
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot }); | ||
|
||
// `ncc` has some issues with `@vercel/fun`'s runtime files: | ||
|
@@ -78,6 +78,10 @@ async function main() { | |
// Band-aid to bundle stuff that `ncc` neglects to bundle | ||
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot); | ||
await cpy(join(dirRoot, 'src/util/dev/builder-worker.js'), distRoot); | ||
await cpy( | ||
join(dirRoot, 'src/util/get-latest-version/get-latest-worker.js'), | ||
distRoot | ||
); | ||
|
||
console.log('Finished building Vercel CLI'); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ import sourceMap from '@zeit/source-map-support'; | |
import { mkdirp } from 'fs-extra'; | ||
import chalk from 'chalk'; | ||
import epipebomb from 'epipebomb'; | ||
import updateNotifier from 'update-notifier'; | ||
import getLatestVersion from './util/get-latest-version'; | ||
import { URL } from 'url'; | ||
import * as Sentry from '@sentry/node'; | ||
import hp from './util/humanize-path'; | ||
|
@@ -55,13 +55,6 @@ import { VercelConfig } from '@vercel/client'; | |
|
||
const isCanary = pkg.version.includes('canary'); | ||
|
||
// Checks for available update and returns an instance | ||
const notifier = updateNotifier({ | ||
pkg, | ||
distTag: isCanary ? 'canary' : 'latest', | ||
updateCheckInterval: 1000 * 60 * 60 * 24 * 7, // 1 week | ||
}); | ||
|
||
const VERCEL_DIR = getGlobalPathConfig(); | ||
const VERCEL_CONFIG_PATH = configFiles.getConfigFilePath(); | ||
const VERCEL_AUTH_CONFIG_PATH = configFiles.getAuthConfigFilePath(); | ||
|
@@ -149,22 +142,30 @@ const main = async () => { | |
} | ||
|
||
// Print update information, if available | ||
if (notifier.update && notifier.update.latest !== pkg.version && isTTY) { | ||
const { latest } = notifier.update; | ||
console.log( | ||
info( | ||
`${chalk.black.bgCyan('UPDATE AVAILABLE')} ` + | ||
`Run ${cmd( | ||
await getUpdateCommand() | ||
)} to install ${getTitleName()} CLI ${latest}` | ||
) | ||
); | ||
if (isTTY && !process.env.NO_UPDATE_NOTIFIER) { | ||
// Check if an update is available. If so, `latest` will contain a string | ||
// of the latest version, otherwise `undefined`. | ||
const latest = getLatestVersion({ | ||
distTag: isCanary ? 'canary' : 'latest', | ||
output, | ||
pkg, | ||
styfle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
cb1kenobi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (latest) { | ||
console.log( | ||
info( | ||
`${chalk.black.bgCyan('UPDATE AVAILABLE')} ` + | ||
`Run ${cmd( | ||
await getUpdateCommand() | ||
)} to install ${getTitleName()} CLI ${latest}` | ||
) | ||
); | ||
|
||
console.log( | ||
info( | ||
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${latest}` | ||
) | ||
); | ||
console.log( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be using I realize that this was the previous behavior as well, but since we're here we might as well fix it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we update this in a follow-up PR? This was about using |
||
`${info( | ||
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${latest}` | ||
)}\n` | ||
); | ||
} | ||
} | ||
|
||
// The second argument to the command can be: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/** | ||
* This file is spawned in the background and checks npm for the latest version | ||
* of the CLI, then writes the version to the cache file. | ||
* | ||
* NOTE: Since this file runs asynchronously in the background, it's possible | ||
* for multiple instances of this file to be running at the same time leading | ||
* to a race condition where the most recent instance will overwrite the | ||
* previous cache file resetting the `notified` flag and cause the update | ||
* notification to appear for multiple consequetive commands. Not the end of | ||
* the world, but something to be aware of. | ||
*/ | ||
|
||
const fetch = require('node-fetch'); | ||
const fs = require('fs-extra'); | ||
const path = require('path'); | ||
const { Agent: HttpsAgent } = require('https'); | ||
const { bold, gray, red } = require('chalk'); | ||
const { format, inspect } = require('util'); | ||
|
||
/** | ||
* An simple output helper which accumulates error and debug log messages in | ||
* memory for potential persistance to disk while immediately outputting errors | ||
* and debug messages, when the `--debug` flag is set, to `stderr`. | ||
*/ | ||
class WorkerOutput { | ||
debugLog = []; | ||
logFile = null; | ||
|
||
constructor({ debug = true }) { | ||
this.debugOutputEnabled = debug; | ||
} | ||
|
||
debug(...args) { | ||
this.print('debug', args); | ||
} | ||
|
||
error(...args) { | ||
this.print('error', args); | ||
} | ||
|
||
print(type, args) { | ||
const str = format( | ||
...args.map(s => (typeof s === 'string' ? s : inspect(s))) | ||
); | ||
this.debugLog.push(`[${new Date().toISOString()}] [${type}] ${str}`); | ||
if (type === 'debug' && this.debugOutputEnabled) { | ||
console.error( | ||
`${gray('>')} ${bold('[debug]')} ${gray( | ||
`[${new Date().toISOString()}]` | ||
)} ${str}` | ||
); | ||
} else if (type === 'error') { | ||
console.error(`${red(`Error:`)} ${str}`); | ||
} | ||
} | ||
|
||
setLogFile(file) { | ||
// wire up the exit handler the first time the log file is set | ||
if (this.logFile === null) { | ||
process.on('exit', () => { | ||
EndangeredMassa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (this.debugLog.length) { | ||
fs.outputFileSync(this.logFile, this.debugLog.join('\n')); | ||
} | ||
}); | ||
} | ||
this.logFile = file; | ||
} | ||
} | ||
|
||
const output = new WorkerOutput({ | ||
// enable the debug logging if the `--debug` is set or if this worker script | ||
// was directly executed | ||
debug: process.argv.includes('--debug') || !process.connected, | ||
EndangeredMassa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
process.on('unhandledRejection', err => { | ||
output.error('Exiting worker due to unhandled rejection:', err); | ||
process.exit(1); | ||
}); | ||
|
||
const defaultGetLatestInterval = 1000 * 60 * 60 * 24 * 7; // 1 week | ||
|
||
// this timer will prevent this worker process from running longer than 10s | ||
const timer = setTimeout(() => { | ||
output.error('Worker timed out after 10 seconds'); | ||
process.exit(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs to clean up the lock file. Otherwise the user will never be able to update again. I'm wondering if we even need a lock file at all 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This We could rename Lock files are only 4KB on disk and I have yet to come across a scenario where the lock file wasn't cleaned up. I'm sure it will happen at some point and that's why I added the stale pid check in As for needing the lock file, I did some testing and it's possible to run |
||
}, 10000); | ||
|
||
// wait for the parent to give us the work payload | ||
process.once('message', async msg => { | ||
output.debug('Received message from parent:', msg); | ||
|
||
output.debug('Disconnecting from parent'); | ||
process.disconnect(); | ||
|
||
const { cacheFile, distTag, name, getLatestInterval } = msg; | ||
const cacheFileParsed = path.parse(cacheFile); | ||
await fs.mkdirp(cacheFileParsed.dir); | ||
|
||
output.setLogFile( | ||
path.join(cacheFileParsed.dir, `${cacheFileParsed.name}.log`) | ||
); | ||
|
||
const lockFile = path.join( | ||
cacheFileParsed.dir, | ||
`${cacheFileParsed.name}.lock` | ||
); | ||
|
||
try { | ||
// check for a lock file and either bail if running or write our pid and continue | ||
output.debug(`Checking lock file: ${lockFile}`); | ||
if (await isRunning(lockFile)) { | ||
output.debug('Worker already running, exiting'); | ||
process.exit(1); | ||
} | ||
output.debug(`Initializing lock file with pid ${process.pid}`); | ||
await fs.writeFile(lockFile, String(process.pid), 'utf-8'); | ||
|
||
// fetch the latest version from npm | ||
const agent = new HttpsAgent({ | ||
keepAlive: true, | ||
maxSockets: 15, // See: `npm config get maxsockets` | ||
}); | ||
const headers = { | ||
accept: | ||
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*', | ||
}; | ||
const url = `https://registry.npmjs.org/${name}`; | ||
output.debug(`Fetching ${url}`); | ||
const res = await fetch(url, { agent, headers }); | ||
const json = await res.json(); | ||
const tags = json['dist-tags']; | ||
const version = tags[distTag]; | ||
|
||
if (version) { | ||
output.debug(`Found dist tag "${distTag}" with version "${version}"`); | ||
} else { | ||
output.error(`Dist tag "${distTag}" not found`); | ||
output.debug('Available dist tags:', Object.keys(tags)); | ||
} | ||
|
||
output.debug(`Writing cache file: ${cacheFile}`); | ||
await fs.outputJSON(cacheFile, { | ||
expireAt: Date.now() + (getLatestInterval || defaultGetLatestInterval), | ||
styfle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
notified: false, | ||
version, | ||
}); | ||
} catch (err) { | ||
output.error(`Failed to get package info:`, err); | ||
} finally { | ||
clearTimeout(timer); | ||
|
||
output.debug(`Releasing lock file: ${lockFile}`); | ||
await fs.remove(lockFile); | ||
|
||
output.debug(`Worker finished successfully!`); | ||
|
||
// force the worker to exit | ||
process.exit(0); | ||
} | ||
}); | ||
|
||
// signal the parent process we're ready | ||
if (process.connected) { | ||
output.debug("Notifying parent we're ready"); | ||
process.send({ type: 'ready' }); | ||
} else { | ||
console.error('No IPC bridge detected, exiting'); | ||
process.exit(1); | ||
} | ||
|
||
async function isRunning(lockFile) { | ||
try { | ||
const pid = parseInt(await fs.readFile(lockFile, 'utf-8')); | ||
output.debug(`Found lock file with pid: ${pid}`); | ||
|
||
// checks for existence of a process; throws if not found | ||
process.kill(pid, 0); | ||
|
||
// process is still running | ||
return true; | ||
} catch (err) { | ||
// lock file does not exist or process is not running and pid is stale | ||
output.debug(`Resetting lock file: ${err.toString()}`); | ||
await fs.remove(lockFile); | ||
return false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
--forceExit
because the update notifier is async and the process will out live the test and causes jest to complain.