diff --git a/__tests__/commands/add.js b/__tests__/commands/add.js index eab7253c5f..b5ba991083 100644 --- a/__tests__/commands/add.js +++ b/__tests__/commands/add.js @@ -40,6 +40,10 @@ test.concurrent('install with arg', (): Promise => { return runAdd({}, ['is-online'], 'install-with-arg'); }); +test.concurrent('install from github', (): Promise => { + return runAdd({}, ['substack/node-mkdirp#master'], 'install-github'); +}); + test.concurrent('install with --dev flag', (): Promise => { return runAdd({dev: true}, ['left-pad@1.1.0'], 'add-with-flag', async (config) => { const lockfile = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock'))); @@ -571,5 +575,4 @@ test.concurrent('add should generate correct integrity file', (): Promise } expect(allCorrect).toBe(true); }); - }); diff --git a/__tests__/commands/install.js b/__tests__/commands/install.js index 1db6709de4..246021a166 100644 --- a/__tests__/commands/install.js +++ b/__tests__/commands/install.js @@ -127,6 +127,10 @@ test.concurrent('install from git cache', (): Promise => { }); }); +test.concurrent('install from github', (): Promise => { + return runInstall({}, 'install-github'); +}); + test.concurrent('install should dedupe dependencies avoiding conflicts 0', (): Promise => { // A@2.0.1 -> B@2.0.0 // B@1.0.0 diff --git a/__tests__/commands/ls.js b/__tests__/commands/ls.js index 5472d7dfd7..cce3fb6ddf 100644 --- a/__tests__/commands/ls.js +++ b/__tests__/commands/ls.js @@ -9,7 +9,6 @@ import Config from '../../src/config.js'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; -const stream = require('stream'); const path = require('path'); const os = require('os'); @@ -48,14 +47,7 @@ async function runLs( } } - let out = ''; - const stdout = new stream.Writable({ - decodeStrings: false, - write(data, encoding, cb) { - out += data; - cb(); - }, - }); + const out = ''; const reporter = new reporters.BufferReporter({stdout: null, stdin: null}); diff --git a/__tests__/commands/run.js b/__tests__/commands/run.js index dcf0eb8c4e..1912e76413 100644 --- a/__tests__/commands/run.js +++ b/__tests__/commands/run.js @@ -12,7 +12,6 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; const execCommand: $FlowFixMe = require('../../src/util/execute-lifecycle-script').execCommand; -const stream = require('stream'); const path = require('path'); const os = require('os'); @@ -40,14 +39,7 @@ async function runRun( } } - let out = ''; - const stdout = new stream.Writable({ - decodeStrings: false, - write(data, encoding, cb) { - out += data; - cb(); - }, - }); + const out = ''; const reporter = new reporters.BufferReporter({stdout: null, stdin: null}); diff --git a/__tests__/fixtures/add/install-github/.gitkeep b/__tests__/fixtures/add/install-github/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__tests__/fixtures/install/install-github/package.json b/__tests__/fixtures/install/install-github/package.json new file mode 100644 index 0000000000..b45169151a --- /dev/null +++ b/__tests__/fixtures/install/install-github/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "mkdirp": "substack/node-mkdirp#master" + } +} diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index f601905fcd..73f8f8fbc4 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -26,9 +26,14 @@ import map from '../../util/map.js'; import {sortAlpha} from '../../util/misc.js'; const invariant = require('invariant'); +const semver = require('semver'); const emoji = require('node-emoji'); +const isCI = require('is-ci'); const path = require('path'); +const {version: YARN_VERSION, installationMethod: YARN_INSTALL_METHOD} = require('../../../package.json'); +const ONE_DAY = 1000 * 60 * 60 * 24; + export type InstallCwdRequest = [ DependencyRequestPatterns, Array, @@ -64,6 +69,47 @@ type Flags = { tilde: boolean, }; +/** + * Try and detect the installation method for Yarn and provide a command to update it with. + */ + +function getUpdateCommand(): ?string { + if (YARN_INSTALL_METHOD === 'tar') { + return 'curl -o- -L https://yarnpkg.com/install.sh | bash'; + } + + if (YARN_INSTALL_METHOD === 'homebrew') { + return 'brew upgrade yarn'; + } + + if (YARN_INSTALL_METHOD === 'deb') { + return 'sudo apt-get update && sudo apt-get install yarn'; + } + + if (YARN_INSTALL_METHOD === 'rpm') { + return 'sudo yum install yarn'; + } + + if (YARN_INSTALL_METHOD === 'npm') { + return 'npm upgrade --global yarn'; + } + + if (YARN_INSTALL_METHOD === 'chocolatey') { + return 'choco upgrade yarn'; + } + + return null; +} + +function getUpdateInstaller(): ?string { + // Windows + if (YARN_INSTALL_METHOD === 'msi') { + return 'https://yarnpkg.com/latest.msi'; + } + + return null; +} + function normalizeFlags(config: Config, rawFlags: Object): Flags { const flags = { // install @@ -279,6 +325,8 @@ export class Install { */ async init(): Promise> { + this.checkUpdate(); + // warn if we have a shrinkwrap if (await fs.exists(path.join(this.config.cwd, 'npm-shrinkwrap.json'))) { this.reporter.error(this.reporter.lang('shrinkwrapWarning')); @@ -356,6 +404,7 @@ export class Install { // fin! await this.saveLockfileAndIntegrity(patterns); + this.maybeOutputUpdate(); this.config.requestManager.clearCache(); return patterns; } @@ -636,6 +685,72 @@ export class Install { return request; } + + /** + * Check for updates every day and output a nag message if there's a newer version. + */ + + checkUpdate() { + if (!process.stdout.isTTY || isCI) { + // don't show upgrade dialog on CI or non-TTY terminals + return; + } + + // only check for updates once a day + const lastUpdateCheck = Number(this.config.getOption('lastUpdateCheck')) || 0; + if (lastUpdateCheck && Date.now() - lastUpdateCheck < ONE_DAY) { + return; + } + + // don't bug for updates on tagged releases + if (YARN_VERSION.indexOf('-') >= 0) { + return; + } + + this._checkUpdate().catch(() => { + // swallow errors + }); + } + + async _checkUpdate(): Promise { + let latestVersion = await this.config.requestManager.request({ + url: 'https://yarnpkg.com/latest-version', + }); + invariant(typeof latestVersion === 'string', 'expected string'); + latestVersion = latestVersion.trim(); + if (!semver.valid(latestVersion)) { + return; + } + + // ensure we only check for updates periodically + this.config.registries.yarn.saveHomeConfig({ + lastUpdateCheck: Date.now(), + }); + + if (semver.gt(latestVersion, YARN_VERSION)) { + this.maybeOutputUpdate = () => { + this.reporter.warn(this.reporter.lang('yarnOutdated', latestVersion, YARN_VERSION)); + + const command = getUpdateCommand(); + if (command) { + this.reporter.info(this.reporter.lang('yarnOutdatedCommand')); + this.reporter.command(command); + } else { + const installer = getUpdateInstaller(); + if (installer) { + this.reporter.info(this.reporter.lang('yarnOutdatedInstaller', installer)); + } + } + }; + } + } + + /** + * Method to override with a possible upgrade message. + */ + + maybeOutputUpdate() {} + maybeOutputUpdate: any; } export function setFlags(commander: Object) { diff --git a/src/package-fetcher.js b/src/package-fetcher.js index 443efd101d..a8c1c88514 100644 --- a/src/package-fetcher.js +++ b/src/package-fetcher.js @@ -88,7 +88,11 @@ export default class PackageFetcher { newPkg = res.package; // update with new remote - ref.remote.hash = res.hash; + // but only if there was a hash previously as the tarball fetcher does not provide a hash. + if (ref.remote.hash) { + ref.remote.hash = res.hash; + } + if (res.resolved) { ref.remote.resolved = res.resolved; }