diff --git a/__tests__/commands/_helpers.js b/__tests__/commands/_helpers.js index 8a4e4ba757..518f2d13e5 100644 --- a/__tests__/commands/_helpers.js +++ b/__tests__/commands/_helpers.js @@ -114,7 +114,7 @@ export async function run( try { const config = new Config(reporter); await config.init({ - binLinks: !!flags.binLinks, + binLinks: typeof flags.binLinks === 'boolean' ? flags.binLinks : true, cwd, globalFolder: path.join(cwd, '.yarn-global'), cacheFolder: flags.cacheFolder || path.join(cwd, '.yarn-cache'), diff --git a/__tests__/commands/global.js b/__tests__/commands/global.js index 46d5f15145..95a4fa93e5 100644 --- a/__tests__/commands/global.js +++ b/__tests__/commands/global.js @@ -1,9 +1,11 @@ /* @flow */ import type {CLIFunctionReturn} from '../../src/types.js'; +import type Config from '../../src/config.js'; import {ConsoleReporter} from '../../src/reporters/index.js'; import {run as buildRun} from './_helpers.js'; import {run as global} from '../../src/cli/commands/global.js'; +import stringify from '../../src/lockfile/stringify.js'; import * as fs from '../../src/util/fs.js'; import assert from 'assert'; @@ -12,17 +14,25 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; const os = require('os'); const path = require('path'); +// call getGlobalPath with the path to GLOBAL_BIN as the 1st argument to get the same path +// for all platforms – path-to-temp-folder/.yarn-tmpbin/bin +const GLOBAL_BIN = process.platform === 'win32' ? path.join('.yarn-tmpbin', 'bin') : '.yarn-tmpbin'; + const fixturesLoc = path.join(__dirname, '..', 'fixtures', 'global'); const runGlobal = buildRun.bind( null, ConsoleReporter, fixturesLoc, (args, flags, config, reporter): CLIFunctionReturn => { + if (typeof flags.prefix === 'undefined' && !flags._noPrefix) { + flags.prefix = path.join(config.cwd, GLOBAL_BIN); + } + delete flags._noPrefix; return global(config, reporter, flags, args); }, ); -function getGlobalPath(prefix, name): string { +function getGlobalPath(prefix, name = ''): string { if (process.platform === 'win32') { return path.join(prefix, name); } else { @@ -34,12 +44,56 @@ function getTempGlobalFolder(): string { return path.join(os.tmpdir(), `yarn-global-${Math.random()}`); } -test.concurrent('add without flag', (): Promise => { - return runGlobal(['add', 'react-native-cli'], {}, 'add-without-flag', async (config) => { - assert.ok(await fs.exists(path.join(config.globalFolder, 'node_modules', 'react-native-cli'))); - assert.ok(await fs.exists(path.join(config.globalFolder, 'node_modules', '.bin', 'react-native'))); - }); -}); +/** + * Test that all the links in the `yarn global bin` folder are from globally + * installed packages and that all the bins from the packages installed + */ +async function testFilesInGlobalBin( + config: Config, + expected: Array, + userCmd?: Array = [], +): Promise { + if (!config.binLinks) { + expected = []; + } + if (process.platform === 'win32') { + expected = expected.reduce((a, b) => { + return a.concat(b, `${b}.cmd`); + }, []); + } + const expectedList = [...expected, ...userCmd].sort().join(','); + + // read the content of the folder + // global function change config.cwd to config.globalFolder, + // go one level up to be on the .yarn folder + const binFolder = getGlobalPath(path.join(config.cwd, '..', GLOBAL_BIN)); + + // when the folder not exist set files to empty array + let files = []; + try { + files = await fs.readdir(binFolder); + } catch (ex) {} + + assert.equal(files.sort().join(','), expectedList); +} + +// Make sure the file was updated by comparing the first 10 characters +async function compareContent(config: Config): Promise { + if (!config.binLinks) { + return; + } + const binFolder = getGlobalPath(path.join(config.cwd, '..', GLOBAL_BIN)); + const expectedFolder = path.join(config.cwd, '..', 'expected-bin-files'); + const files = await fs.readdir(expectedFolder); + for (const file of files) { + const [win, cmd] = [process.platform === 'win32', file.endsWith('.cmd')]; + if (win && cmd || !win && !cmd) { + const actual = await fs.readFile(path.join(binFolder, file)); + const expected = await fs.readFile(path.join(expectedFolder, file)); + assert.equal(actual.substr(0, 10), expected.substr(0, 10)); + } + } +} test.concurrent('add with prefix flag', (): Promise => { const tmpGlobalFolder = getTempGlobalFolder(); @@ -48,14 +102,81 @@ test.concurrent('add with prefix flag', (): Promise => { }); }); +test.concurrent('add with .yarnrc file', (): Promise => { + const tmpGlobalFolder = getTempGlobalFolder(); + return runGlobal(['add', 'react-native-cli'], {_noPrefix: true}, 'add-with-yarnrc-file', async (config) => { + assert.ok(await fs.exists(getGlobalPath(tmpGlobalFolder, 'react-native'))); + }, async (cwd) => { + // create .yarnrc file and place it in .yarn-global + const loc = path.join(cwd, '.yarn-global', '.yarnrc'); + await fs.mkdirp(path.join(cwd, '.yarn-global')); + await fs.writeFilePreservingEol(loc, `${stringify({prefix: tmpGlobalFolder})}\n`); + }); +}); + // don't run this test in `concurrent`, it will affect other tests -test('add with PREFIX enviroment variable', (): Promise => { +test('add with PREFIX environment variable', (): Promise => { const tmpGlobalFolder = getTempGlobalFolder(); const envPrefix = process.env.PREFIX; process.env.PREFIX = tmpGlobalFolder; - return runGlobal(['add', 'react-native-cli'], {}, 'add-with-prefix-env', async (config) => { - assert.ok(await fs.exists(getGlobalPath(tmpGlobalFolder, 'react-native'))); + return runGlobal(['add', 'react-native-cli'], {_noPrefix: true}, 'add-with-prefix-env', async (config) => { // restore env process.env.PREFIX = envPrefix; + assert.ok(await fs.exists(getGlobalPath(tmpGlobalFolder, 'react-native'))); }); }); + +function globalAddBins(binLinks): Function { + return (): Promise => { + return runGlobal(['add', 'react-native-cli'], {binLinks}, 'global-add-with-bin', async (config) => { + assert.ok(await fs.exists(path.join(config.cwd, 'node_modules', 'react-native-cli'))); + const binExist = await fs.exists(path.join(config.cwd, 'node_modules', '.bin', 'react-native')); + assert.ok(config.binLinks ? binExist : !binExist); + await testFilesInGlobalBin(config, ['react-native']); + await compareContent(config); + }); + }; +} + +function globalRemoveBins(binLinks): Function { + return (): Promise => { + // A@1 -> B@1 + // C@1 + // react-native - missing bin + + // remove A + + // result.... + // C@1 - without bin + // react-native - with bin + const name = 'global-remove'; + return runGlobal(['remove', 'dep-a'], {binLinks}, name, async (config, reporter) => { + // the link for react-native was missing frol the files in the fixtures folder, + // we expect it to be installed + await testFilesInGlobalBin(config, ['react-native'], ['user-command']); + assert.ok(!await fs.exists(path.join(config.cwd, 'node_modules/dep-a'))); + assert.ok(!await fs.exists(path.join(config.cwd, 'node_modules/dep-b'))); + assert.ok(await fs.exists(path.join(config.cwd, 'node_modules/dep-c'))); + }); + }; +} + +function globalUpgradeBins(binLinks): Function { + return (): Promise => { + const name = 'global-update-with-bin'; + return runGlobal(['upgrade', 'react-native-cli'], {binLinks}, name, async (config) => { + await testFilesInGlobalBin(config, ['react-native']); + await compareContent(config); + }); + }; +} + +// flags.binLinks = true +test.concurrent('global add: package with bin, flags: {binlinks: true}', globalAddBins(true)); +test.concurrent('global remove: dependencies with bins, flags: {binlinks: true}', globalRemoveBins(true)); +test.concurrent('global upgrade: package with bin, flags: {binlinks: true}', globalUpgradeBins(true)); + +// flags.binLinks = false +test.concurrent('global add: package with bin, flags: {binlinks: false}', globalAddBins(false)); +test.concurrent('global remove: dependencies with bins, flags: {binlinks: false}', globalRemoveBins(false)); +test.concurrent('global upgrade: package with bin, flags: {binlinks: false}', globalUpgradeBins(false)); diff --git a/__tests__/commands/install/integration.js b/__tests__/commands/install/integration.js index 297ba9cec5..c03ec9a994 100644 --- a/__tests__/commands/install/integration.js +++ b/__tests__/commands/install/integration.js @@ -6,7 +6,8 @@ import * as reporters from '../../../src/reporters/index.js'; import {Install} from '../../../src/cli/commands/install.js'; import Lockfile from '../../../src/lockfile/wrapper.js'; import * as fs from '../../../src/util/fs.js'; -import {getPackageVersion, explodeLockfile, runInstall, createLockfile} from '../_helpers.js'; +import {ConsoleReporter} from '../../../src/reporters/index.js'; +import {getPackageVersion, explodeLockfile, createLockfile, runInstall, run as buildRun} from '../_helpers.js'; import {promisify} from '../../../src/util/promise'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; @@ -18,6 +19,8 @@ const fsNode = require('fs'); const path = require('path'); const os = require('os'); +const fixturesLoc = path.join(__dirname, '..', '..', 'fixtures', 'install'); + beforeEach(request.__resetAuthedRequests); // $FlowFixMe afterEach(request.__resetAuthedRequests); @@ -452,15 +455,39 @@ test.concurrent( }, ); -// disabled to resolve https://github.com/yarnpkg/yarn/pull/1210 -test.skip('install should hoist nested bin scripts', (): Promise => { - return runInstall({binLinks: true}, 'install-nested-bin', async (config) => { +test.concurrent('install should hoist nested bin scripts', (): Promise => { + // we mock linkBin to enable us to test it + let mockLinkBin; + + const run = buildRun.bind( + null, + ConsoleReporter, + fixturesLoc, + async (args, flags, config, reporter, lockfile): Promise => { + const install = new Install(flags, config, reporter, lockfile); + const linkBin = install.linker.linkBin; + install.linker.linkBin = mockLinkBin = jest.fn((...args) => { + return linkBin(...args); + }); + await install.init(); + await check(config, reporter, {}, []); + return install; + }, + [], + ); + + return run({binLinks: true}, 'install-nested-bin', async (config) => { const binScripts = await fs.walk(path.join(config.cwd, 'node_modules', '.bin')); // need to double the amount as windows makes 2 entries for each dependency // so for below, there would be an entry for eslint and eslint.cmd on win32 const amount = process.platform === 'win32' ? 20 : 10; assert.equal(binScripts.length, amount); assert(binScripts.findIndex((f) => f.basename === 'eslint') > -1); + + // linkBin should be called once for each bin + const locations = mockLinkBin.mock.calls.map((call) => call[0]); + const uniqueResult = locations.filter((loc, index, arr) => arr.indexOf(loc) === index); + assert.equal(locations.length, uniqueResult.length); }); }); diff --git a/__tests__/commands/remove.js b/__tests__/commands/remove.js index 01ee88672e..d201c6c4e7 100644 --- a/__tests__/commands/remove.js +++ b/__tests__/commands/remove.js @@ -1,5 +1,6 @@ /* @flow */ +import type {Install} from '../../src/cli/commands/install.js'; import {ConsoleReporter} from '../../src/reporters/index.js'; import {run as buildRun, explodeLockfile} from './_helpers.js'; import {run as remove} from '../../src/cli/commands/remove.js'; @@ -12,9 +13,12 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; const path = require('path'); const fixturesLoc = path.join(__dirname, '..', 'fixtures', 'remove'); -const runRemove = buildRun.bind(null, ConsoleReporter, fixturesLoc, (args, flags, config, reporter): Promise => { - return remove(config, reporter, flags, args); -}); +const runRemove = buildRun.bind( + null, + ConsoleReporter, + fixturesLoc, + (args, flags, config, reporter): Promise => remove(config, reporter, flags, args), +); test.concurrent('throws error with no arguments', (): Promise => { const reporter = new reporters.ConsoleReporter({}); diff --git a/__tests__/commands/upgrade.js b/__tests__/commands/upgrade.js index cbe5f51a53..fdaab2c392 100644 --- a/__tests__/commands/upgrade.js +++ b/__tests__/commands/upgrade.js @@ -1,5 +1,6 @@ /* @flow */ +import type {Add} from '../../src/cli/commands/add.js'; import {ConsoleReporter} from '../../src/reporters/index.js'; import {explodeLockfile, run as buildRun} from './_helpers.js'; import {run as upgrade} from '../../src/cli/commands/upgrade.js'; @@ -12,9 +13,12 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; const path = require('path'); const fixturesLoc = path.join(__dirname, '..', 'fixtures', 'upgrade'); -const runUpgrade = buildRun.bind(null, ConsoleReporter, fixturesLoc, (args, flags, config, reporter): Promise => { - return upgrade(config, reporter, flags, args); -}); +const runUpgrade = buildRun.bind( + null, + ConsoleReporter, + fixturesLoc, + (args, flags, config, reporter): Promise => upgrade(config, reporter, flags, args), +); test.concurrent('throws if lockfile is out of date', (): Promise => { const reporter = new reporters.ConsoleReporter({}); diff --git a/__tests__/fixtures/global/add-with-prefix-env/.yarn-global/.yarnrc b/__tests__/fixtures/global/add-with-prefix-env/.yarn-global/.yarnrc new file mode 100644 index 0000000000..3100b0f46e --- /dev/null +++ b/__tests__/fixtures/global/add-with-prefix-env/.yarn-global/.yarnrc @@ -0,0 +1 @@ +prefix "" diff --git a/__tests__/fixtures/global/add-with-yarnrc-file/.gitkeep b/__tests__/fixtures/global/add-with-yarnrc-file/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__tests__/fixtures/global/global-add-with-bin/.yarn-global/.gitkeep b/__tests__/fixtures/global/global-add-with-bin/.yarn-global/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native b/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native new file mode 100644 index 0000000000..908ba8417a --- /dev/null +++ b/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native @@ -0,0 +1 @@ +#!/usr/bin/env node diff --git a/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native.cmd b/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native.cmd new file mode 100644 index 0000000000..cfa4635dea --- /dev/null +++ b/__tests__/fixtures/global/global-add-with-bin/expected-bin-files/react-native.cmd @@ -0,0 +1 @@ +@IF EXIST "%~dp0\node.exe" diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/.npmrc b/__tests__/fixtures/global/global-remove/.yarn-global/.npmrc new file mode 100644 index 0000000000..9465b97ac3 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/.npmrc @@ -0,0 +1 @@ +yarn-offline-mirror=./mirror-for-offline diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/mirror-for-offline/dep-c-1.0.0.tgz b/__tests__/fixtures/global/global-remove/.yarn-global/mirror-for-offline/dep-c-1.0.0.tgz new file mode 100644 index 0000000000..cf80b68d32 Binary files /dev/null and b/__tests__/fixtures/global/global-remove/.yarn-global/mirror-for-offline/dep-c-1.0.0.tgz differ diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.bin/dep-a b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.bin/dep-a new file mode 100644 index 0000000000..5d05f0fda0 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.bin/dep-a @@ -0,0 +1,2 @@ +# test file line 1 +# test file line 2 \ No newline at end of file diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.yarn-integrity b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.yarn-integrity new file mode 100644 index 0000000000..ec42d7cb43 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/.yarn-integrity @@ -0,0 +1 @@ +a6246e0c1fe9a61c65f28219ab6b96dbcbfb942927767c18bc16ae53e1a17776 \ No newline at end of file diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/index.js b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/index.js new file mode 100644 index 0000000000..24e9126969 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/index.js @@ -0,0 +1,33 @@ + +/** + * isArray + */ + +var isArray = Array.isArray; + +/** + * toString + */ + +var str = Object.prototype.toString; + +/** + * Whether or not the given `val` + * is an array. + * + * example: + * + * isArray([]); + * // > true + * isArray(arguments); + * // > false + * isArray(''); + * // > false + * + * @param {mixed} val + * @return {bool} + */ + +module.exports = isArray || function (val) { + return !! val && '[object Array]' == str.call(val); +}; diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/package.json b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/package.json new file mode 100644 index 0000000000..52c45454b8 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-a/package.json @@ -0,0 +1,14 @@ + +{ + "name": "dep-a", + "description": "Check if the given value is an Array", + "version": "1.0.0", + "keywords": ["isArray", "es5", "array"], + "dependencies": { + "dep-b": "1.0.0" + }, + "bin": { + "dep-a": "index.js" + }, + "license": "MIT" +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/index.js b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/index.js new file mode 100644 index 0000000000..24e9126969 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/index.js @@ -0,0 +1,33 @@ + +/** + * isArray + */ + +var isArray = Array.isArray; + +/** + * toString + */ + +var str = Object.prototype.toString; + +/** + * Whether or not the given `val` + * is an array. + * + * example: + * + * isArray([]); + * // > true + * isArray(arguments); + * // > false + * isArray(''); + * // > false + * + * @param {mixed} val + * @return {bool} + */ + +module.exports = isArray || function (val) { + return !! val && '[object Array]' == str.call(val); +}; diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/package.json b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/package.json new file mode 100644 index 0000000000..8f5d671959 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-b/package.json @@ -0,0 +1,13 @@ + +{ + "name": "dep-b", + "description": "Check if the given value is an Array", + "version": "1.0.0", + "keywords": ["isArray", "es5", "array"], + "dependencies": { + }, + "bin": { + "dep-b": "index.js" + }, + "license": "MIT" +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/index.js b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/index.js new file mode 100644 index 0000000000..24e9126969 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/index.js @@ -0,0 +1,33 @@ + +/** + * isArray + */ + +var isArray = Array.isArray; + +/** + * toString + */ + +var str = Object.prototype.toString; + +/** + * Whether or not the given `val` + * is an array. + * + * example: + * + * isArray([]); + * // > true + * isArray(arguments); + * // > false + * isArray(''); + * // > false + * + * @param {mixed} val + * @return {bool} + */ + +module.exports = isArray || function (val) { + return !! val && '[object Array]' == str.call(val); +}; diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/package.json b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/package.json new file mode 100644 index 0000000000..dd7cdbb98e --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/dep-c/package.json @@ -0,0 +1,10 @@ + +{ + "name": "dep-c", + "description": "Check if the given value is an Array", + "version": "1.0.0", + "keywords": ["isArray", "es5", "array"], + "dependencies": { + }, + "license": "MIT" +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/index.js b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/index.js new file mode 100644 index 0000000000..fa1e5fc4ea --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/index.js @@ -0,0 +1,317 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// /!\ DO NOT MODIFY THIS FILE /!\ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// react-native-cli is installed globally on people's computers. This means +// that it is extremely difficult to have them upgrade the version and +// because there's only one global version installed, it is very prone to +// breaking changes. +// +// The only job of react-native-cli is to init the repository and then +// forward all the commands to the local version of react-native. +// +// If you need to add a new command, please add it to local-cli/. +// +// The only reason to modify this file is to add more warnings and +// troubleshooting information for the `react-native init` command. +// +// Do not make breaking changes! We absolutely don't want to have to +// tell people to update their global version of react-native-cli. +// +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// /!\ DO NOT MODIFY THIS FILE /!\ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var exec = require('child_process').exec; +var execSync = require('child_process').execSync; +var spawn = require('child_process').spawn; +var chalk = require('chalk'); +var prompt = require('prompt'); +var semver = require('semver'); +/** + * Used arguments: + * -v --version - to print current version of react-native-cli and react-native dependency + * if you are in a RN app folder + * init - to create a new project and npm install it + * --verbose - to print logs while init + * --version - override default (https://registry.npmjs.org/react-native@latest), + * package to install, examples: + * - "0.22.0-rc1" - A new app will be created using a specific version of React Native from npm repo + * - "https://registry.npmjs.org/react-native/-/react-native-0.20.0.tgz" - a .tgz archive from any npm repo + * - "/Users/home/react-native/react-native-0.22.0.tgz" - for package prepared with `npm pack`, useful for e2e tests + */ +var argv = require('minimist')(process.argv.slice(2)); + +var CLI_MODULE_PATH = function() { + return path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'cli.js' + ); +}; + +var REACT_NATIVE_PACKAGE_JSON_PATH = function() { + return path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'package.json' + ); +}; + +// Use Yarn if available, it's much faster than the npm client. +// Return the version of yarn installed on the system, null if yarn is not available. +function getYarnVersionIfAvailable() { + let yarnVersion; + try { + // execSync returns a Buffer -> convert to string + if (process.platform.startsWith('win')) { + yarnVersion = (execSync('yarn --version').toString() || '').trim(); + } else { + yarnVersion = (execSync('yarn --version 2>/dev/null').toString() || '').trim(); + } + } catch (error) { + return null; + } + // yarn < 0.16 has a 'missing manifest' bug + try { + if (semver.gte(yarnVersion, '0.16.0')) { + return yarnVersion; + } else { + return null; + } + } catch (error) { + console.error('Cannot parse yarn version: ' + yarnVersion); + return null; + } +} + +checkForVersionArgument(); + +var cli; +var cliPath = CLI_MODULE_PATH(); +if (fs.existsSync(cliPath)) { + cli = require(cliPath); +} + +// minimist api +var commands = argv._; +if (cli) { + cli.run(); +} else { + if (commands.length === 0) { + console.error( + 'You did not pass any commands, did you mean to run `react-native init`?' + ); + process.exit(1); + } + + switch (commands[0]) { + case 'init': + if (!commands[1]) { + console.error( + 'Usage: react-native init [--verbose]' + ); + process.exit(1); + } else { + const rnPackage = argv.version; + init(commands[1], argv.verbose, rnPackage, argv.npm); + } + break; + default: + console.error( + 'Command `%s` unrecognized. ' + + 'Make sure that you have run `npm install` and that you are inside a react-native project.', + commands[0] + ); + process.exit(1); + break; + } +} + +function validateProjectName(name) { + if (!name.match(/^[$A-Z_][0-9A-Z_$]*$/i)) { + console.error( + '"%s" is not a valid name for a project. Please use a valid identifier ' + + 'name (alphanumeric).', + name + ); + process.exit(1); + } + + if (name === 'React') { + console.error( + '"%s" is not a valid name for a project. Please do not use the ' + + 'reserved word "React".', + name + ); + process.exit(1); + } +} + +/** + * @param name Project name, e.g. 'AwesomeApp'. + * @param verbose If true, will run 'npm install' in verbose mode (for debugging). + * @param rnPackage Version of React Native to install, e.g. '0.38.0'. + * @param forceNpmClient If true, always use the npm command line client, + * don't use yarn even if available. + */ +function init(name, verbose, rnPackage, forceNpmClient) { + validateProjectName(name); + + if (fs.existsSync(name)) { + createAfterConfirmation(name, verbose, rnPackage, forceNpmClient); + } else { + createProject(name, verbose, rnPackage, forceNpmClient); + } +} + +function createAfterConfirmation(name, verbose, rnPackage, forceNpmClient) { + prompt.start(); + + var property = { + name: 'yesno', + message: 'Directory ' + name + ' already exists. Continue?', + validator: /y[es]*|n[o]?/, + warning: 'Must respond yes or no', + default: 'no' + }; + + prompt.get(property, function (err, result) { + if (result.yesno[0] === 'y') { + createProject(name, verbose, rnPackage, forceNpmClient); + } else { + console.log('Project initialization canceled'); + process.exit(); + } + }); +} + +function createProject(name, verbose, rnPackage, forceNpmClient) { + var root = path.resolve(name); + var projectName = path.basename(root); + + console.log( + 'This will walk you through creating a new React Native project in', + root + ); + + if (!fs.existsSync(root)) { + fs.mkdirSync(root); + } + + var packageJson = { + name: projectName, + version: '0.0.1', + private: true, + scripts: { + start: 'node node_modules/react-native/local-cli/cli.js start' + } + }; + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson)); + process.chdir(root); + + if (verbose) { + runVerbose(root, projectName, rnPackage, forceNpmClient); + } else { + run(root, projectName, rnPackage, forceNpmClient); + } +} + +function getInstallPackage(rnPackage) { + var packageToInstall = 'react-native'; + var isValidSemver = semver.valid(rnPackage); + if (isValidSemver) { + packageToInstall += '@' + isValidSemver; + } else if (rnPackage) { + // for tar.gz or alternative paths + packageToInstall = rnPackage; + } + return packageToInstall; +} + +function run(root, projectName, rnPackage, forceNpmClient) { + const yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable(); + let installCommand; + if (yarnVersion) { + console.log('Using yarn v' + yarnVersion); + console.log('Installing ' + getInstallPackage(rnPackage) + '...'); + installCommand = 'yarn add ' + getInstallPackage(rnPackage) + ' --exact'; + } else { + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm...'); + if (!forceNpmClient) { + console.log('Consider installing yarn to make this faster: https://yarnpkg.com'); + } + installCommand = 'npm install --save --save-exact ' + getInstallPackage(rnPackage); + } + exec(installCommand, function(err, stdout, stderr) { + if (err) { + console.log(stdout); + console.error(stderr); + console.error('Command `' + installCommand + '` failed.'); + process.exit(1); + } + checkNodeVersion(); + cli = require(CLI_MODULE_PATH()); + cli.init(root, projectName); + }); +} + +function runVerbose(root, projectName, rnPackage, forceNpmClient) { + // Use npm client, yarn doesn't support --verbose yet + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm. This might take a while...'); + var proc = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['install', '--verbose', '--save', '--save-exact', getInstallPackage(rnPackage)], {stdio: 'inherit'}); + proc.on('close', function (code) { + if (code !== 0) { + console.error('`npm install --save --save-exact react-native` failed'); + return; + } + + cli = require(CLI_MODULE_PATH()); + cli.init(root, projectName); + }); +} + +function checkNodeVersion() { + var packageJson = require(REACT_NATIVE_PACKAGE_JSON_PATH()); + if (!packageJson.engines || !packageJson.engines.node) { + return; + } + if (!semver.satisfies(process.version, packageJson.engines.node)) { + console.error(chalk.red( + 'You are currently running Node %s but React Native requires %s. ' + + 'Please use a supported version of Node.\n' + + 'See https://facebook.github.io/react-native/docs/getting-started.html' + ), + process.version, + packageJson.engines.node); + } +} + +function checkForVersionArgument() { + if (argv._.length === 0 && (argv.v || argv.version)) { + console.log('react-native-cli: ' + require('./package.json').version); + try { + console.log('react-native: ' + require(REACT_NATIVE_PACKAGE_JSON_PATH()).version); + } catch (e) { + console.log('react-native: n/a - not inside a React Native project directory'); + } + process.exit(); + } +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/package.json b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/package.json new file mode 100644 index 0000000000..b67d0ec015 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/node_modules/react-native-cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "react-native-cli", + "version": "1.2.0", + "license": "BSD-3-Clause", + "description": "The React Native CLI tools", + "main": "index.js", + "engines": { + "node": ">=4" + }, + "repository": { + "type": "git", + "url": "https://github.com/facebook/react-native.git" + }, + "scripts": { + "test": "mocha" + }, + "bin": { + "react-native": "index.js" + }, + "dependencies": { + "chalk": "^1.1.1", + "minimist": "^1.2.0", + "prompt": "^0.2.14", + "semver": "^5.0.3" + } +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/package.json b/__tests__/fixtures/global/global-remove/.yarn-global/package.json new file mode 100644 index 0000000000..f63ae3924f --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/package.json @@ -0,0 +1,8 @@ +{ + "license": "ISC", + "dependencies": { + "dep-a": "^1.0.0", + "dep-c": "^1.0.0", + "react-native-cli": "^1.0.0" + } +} diff --git a/__tests__/fixtures/global/global-remove/.yarn-global/yarn.lock b/__tests__/fixtures/global/global-remove/.yarn-global/yarn.lock new file mode 100644 index 0000000000..2262c50a4f --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-global/yarn.lock @@ -0,0 +1,16 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +dep-a@^1.0.0: + version "1.0.0" + resolved dep-a-1.0.0.tgz#d170a960654001b74bdee4747e71f744ecf0a24f + dependencies: + dep-b "1.0.0" + +dep-b@1.0.0: + version "1.0.0" + resolved dep-b-1.0.0.tgz#fa3fab4e36d8eb93ac74790748a30547e9cb0f3f + +dep-c@^1.0.0: + version "1.0.0" + resolved dep-c-1.0.0.tgz#87be3f12250ea4b3a060a80663fe376670710775 + diff --git a/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/dep-a b/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/dep-a new file mode 100644 index 0000000000..4de312234b --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/dep-a @@ -0,0 +1 @@ +# global remove should remove this file diff --git a/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/user-command b/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/user-command new file mode 100644 index 0000000000..00ebdf4971 --- /dev/null +++ b/__tests__/fixtures/global/global-remove/.yarn-tmpbin/bin/user-command @@ -0,0 +1 @@ +# global remove should not change this file diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/.npmrc b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/.npmrc new file mode 100644 index 0000000000..9465b97ac3 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/.npmrc @@ -0,0 +1 @@ +yarn-offline-mirror=./mirror-for-offline diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/dep-a b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/dep-a new file mode 100644 index 0000000000..5d05f0fda0 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/dep-a @@ -0,0 +1,2 @@ +# test file line 1 +# test file line 2 \ No newline at end of file diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/react-native b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/react-native new file mode 100644 index 0000000000..7c32de8272 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.bin/react-native @@ -0,0 +1,15 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + "$basedir/node" "$basedir/../.yarn-global/node_modules/react-native-cli/index.js" "$@" + ret=$? +else + node "$basedir/../.yarn-global/node_modules/react-native-cli/index.js" "$@" + ret=$? +fi +exit $ret diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.yarn-integrity b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.yarn-integrity new file mode 100644 index 0000000000..ec42d7cb43 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/.yarn-integrity @@ -0,0 +1 @@ +a6246e0c1fe9a61c65f28219ab6b96dbcbfb942927767c18bc16ae53e1a17776 \ No newline at end of file diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/index.js b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/index.js new file mode 100644 index 0000000000..fa1e5fc4ea --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/index.js @@ -0,0 +1,317 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// /!\ DO NOT MODIFY THIS FILE /!\ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// react-native-cli is installed globally on people's computers. This means +// that it is extremely difficult to have them upgrade the version and +// because there's only one global version installed, it is very prone to +// breaking changes. +// +// The only job of react-native-cli is to init the repository and then +// forward all the commands to the local version of react-native. +// +// If you need to add a new command, please add it to local-cli/. +// +// The only reason to modify this file is to add more warnings and +// troubleshooting information for the `react-native init` command. +// +// Do not make breaking changes! We absolutely don't want to have to +// tell people to update their global version of react-native-cli. +// +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// /!\ DO NOT MODIFY THIS FILE /!\ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var exec = require('child_process').exec; +var execSync = require('child_process').execSync; +var spawn = require('child_process').spawn; +var chalk = require('chalk'); +var prompt = require('prompt'); +var semver = require('semver'); +/** + * Used arguments: + * -v --version - to print current version of react-native-cli and react-native dependency + * if you are in a RN app folder + * init - to create a new project and npm install it + * --verbose - to print logs while init + * --version - override default (https://registry.npmjs.org/react-native@latest), + * package to install, examples: + * - "0.22.0-rc1" - A new app will be created using a specific version of React Native from npm repo + * - "https://registry.npmjs.org/react-native/-/react-native-0.20.0.tgz" - a .tgz archive from any npm repo + * - "/Users/home/react-native/react-native-0.22.0.tgz" - for package prepared with `npm pack`, useful for e2e tests + */ +var argv = require('minimist')(process.argv.slice(2)); + +var CLI_MODULE_PATH = function() { + return path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'cli.js' + ); +}; + +var REACT_NATIVE_PACKAGE_JSON_PATH = function() { + return path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'package.json' + ); +}; + +// Use Yarn if available, it's much faster than the npm client. +// Return the version of yarn installed on the system, null if yarn is not available. +function getYarnVersionIfAvailable() { + let yarnVersion; + try { + // execSync returns a Buffer -> convert to string + if (process.platform.startsWith('win')) { + yarnVersion = (execSync('yarn --version').toString() || '').trim(); + } else { + yarnVersion = (execSync('yarn --version 2>/dev/null').toString() || '').trim(); + } + } catch (error) { + return null; + } + // yarn < 0.16 has a 'missing manifest' bug + try { + if (semver.gte(yarnVersion, '0.16.0')) { + return yarnVersion; + } else { + return null; + } + } catch (error) { + console.error('Cannot parse yarn version: ' + yarnVersion); + return null; + } +} + +checkForVersionArgument(); + +var cli; +var cliPath = CLI_MODULE_PATH(); +if (fs.existsSync(cliPath)) { + cli = require(cliPath); +} + +// minimist api +var commands = argv._; +if (cli) { + cli.run(); +} else { + if (commands.length === 0) { + console.error( + 'You did not pass any commands, did you mean to run `react-native init`?' + ); + process.exit(1); + } + + switch (commands[0]) { + case 'init': + if (!commands[1]) { + console.error( + 'Usage: react-native init [--verbose]' + ); + process.exit(1); + } else { + const rnPackage = argv.version; + init(commands[1], argv.verbose, rnPackage, argv.npm); + } + break; + default: + console.error( + 'Command `%s` unrecognized. ' + + 'Make sure that you have run `npm install` and that you are inside a react-native project.', + commands[0] + ); + process.exit(1); + break; + } +} + +function validateProjectName(name) { + if (!name.match(/^[$A-Z_][0-9A-Z_$]*$/i)) { + console.error( + '"%s" is not a valid name for a project. Please use a valid identifier ' + + 'name (alphanumeric).', + name + ); + process.exit(1); + } + + if (name === 'React') { + console.error( + '"%s" is not a valid name for a project. Please do not use the ' + + 'reserved word "React".', + name + ); + process.exit(1); + } +} + +/** + * @param name Project name, e.g. 'AwesomeApp'. + * @param verbose If true, will run 'npm install' in verbose mode (for debugging). + * @param rnPackage Version of React Native to install, e.g. '0.38.0'. + * @param forceNpmClient If true, always use the npm command line client, + * don't use yarn even if available. + */ +function init(name, verbose, rnPackage, forceNpmClient) { + validateProjectName(name); + + if (fs.existsSync(name)) { + createAfterConfirmation(name, verbose, rnPackage, forceNpmClient); + } else { + createProject(name, verbose, rnPackage, forceNpmClient); + } +} + +function createAfterConfirmation(name, verbose, rnPackage, forceNpmClient) { + prompt.start(); + + var property = { + name: 'yesno', + message: 'Directory ' + name + ' already exists. Continue?', + validator: /y[es]*|n[o]?/, + warning: 'Must respond yes or no', + default: 'no' + }; + + prompt.get(property, function (err, result) { + if (result.yesno[0] === 'y') { + createProject(name, verbose, rnPackage, forceNpmClient); + } else { + console.log('Project initialization canceled'); + process.exit(); + } + }); +} + +function createProject(name, verbose, rnPackage, forceNpmClient) { + var root = path.resolve(name); + var projectName = path.basename(root); + + console.log( + 'This will walk you through creating a new React Native project in', + root + ); + + if (!fs.existsSync(root)) { + fs.mkdirSync(root); + } + + var packageJson = { + name: projectName, + version: '0.0.1', + private: true, + scripts: { + start: 'node node_modules/react-native/local-cli/cli.js start' + } + }; + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson)); + process.chdir(root); + + if (verbose) { + runVerbose(root, projectName, rnPackage, forceNpmClient); + } else { + run(root, projectName, rnPackage, forceNpmClient); + } +} + +function getInstallPackage(rnPackage) { + var packageToInstall = 'react-native'; + var isValidSemver = semver.valid(rnPackage); + if (isValidSemver) { + packageToInstall += '@' + isValidSemver; + } else if (rnPackage) { + // for tar.gz or alternative paths + packageToInstall = rnPackage; + } + return packageToInstall; +} + +function run(root, projectName, rnPackage, forceNpmClient) { + const yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable(); + let installCommand; + if (yarnVersion) { + console.log('Using yarn v' + yarnVersion); + console.log('Installing ' + getInstallPackage(rnPackage) + '...'); + installCommand = 'yarn add ' + getInstallPackage(rnPackage) + ' --exact'; + } else { + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm...'); + if (!forceNpmClient) { + console.log('Consider installing yarn to make this faster: https://yarnpkg.com'); + } + installCommand = 'npm install --save --save-exact ' + getInstallPackage(rnPackage); + } + exec(installCommand, function(err, stdout, stderr) { + if (err) { + console.log(stdout); + console.error(stderr); + console.error('Command `' + installCommand + '` failed.'); + process.exit(1); + } + checkNodeVersion(); + cli = require(CLI_MODULE_PATH()); + cli.init(root, projectName); + }); +} + +function runVerbose(root, projectName, rnPackage, forceNpmClient) { + // Use npm client, yarn doesn't support --verbose yet + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm. This might take a while...'); + var proc = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['install', '--verbose', '--save', '--save-exact', getInstallPackage(rnPackage)], {stdio: 'inherit'}); + proc.on('close', function (code) { + if (code !== 0) { + console.error('`npm install --save --save-exact react-native` failed'); + return; + } + + cli = require(CLI_MODULE_PATH()); + cli.init(root, projectName); + }); +} + +function checkNodeVersion() { + var packageJson = require(REACT_NATIVE_PACKAGE_JSON_PATH()); + if (!packageJson.engines || !packageJson.engines.node) { + return; + } + if (!semver.satisfies(process.version, packageJson.engines.node)) { + console.error(chalk.red( + 'You are currently running Node %s but React Native requires %s. ' + + 'Please use a supported version of Node.\n' + + 'See https://facebook.github.io/react-native/docs/getting-started.html' + ), + process.version, + packageJson.engines.node); + } +} + +function checkForVersionArgument() { + if (argv._.length === 0 && (argv.v || argv.version)) { + console.log('react-native-cli: ' + require('./package.json').version); + try { + console.log('react-native: ' + require(REACT_NATIVE_PACKAGE_JSON_PATH()).version); + } catch (e) { + console.log('react-native: n/a - not inside a React Native project directory'); + } + process.exit(); + } +} diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/package.json b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/package.json new file mode 100644 index 0000000000..894186f842 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/node_modules/react-native-cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "react-native-cli", + "version": "1.0.0", + "license": "BSD-3-Clause", + "description": "The React Native CLI tools", + "main": "index.js", + "engines": { + "node": ">=4" + }, + "repository": { + "type": "git", + "url": "https://github.com/facebook/react-native.git" + }, + "scripts": { + "test": "mocha" + }, + "bin": { + "react-native": "index.js" + }, + "dependencies": { + "chalk": "^1.1.1", + "minimist": "^1.2.0", + "prompt": "^0.2.14", + "semver": "^5.0.3" + } +} diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/package.json b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/package.json new file mode 100644 index 0000000000..f68edc3285 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/package.json @@ -0,0 +1,6 @@ +{ + "license": "ISC", + "dependencies": { + "react-native-cli": "^1.0.0" + } +} diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-global/yarn.lock b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/yarn.lock new file mode 100644 index 0000000000..1affdba201 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-global/yarn.lock @@ -0,0 +1,11 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +react-native-cli@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-native-cli/-/react-native-cli-1.0.0.tgz#112d56c18b7324969d21282a240e3de8903aa963" + dependencies: + chalk "^1.1.1" + minimist "^1.2.0" + prompt "^0.2.14" + semver "^5.0.3" diff --git a/__tests__/fixtures/global/global-update-with-bin/.yarn-tmpbin/bin/react-native b/__tests__/fixtures/global/global-update-with-bin/.yarn-tmpbin/bin/react-native new file mode 100644 index 0000000000..af6b0bbc44 --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/.yarn-tmpbin/bin/react-native @@ -0,0 +1 @@ +# global update should update this file diff --git a/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native b/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native new file mode 100644 index 0000000000..908ba8417a --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native @@ -0,0 +1 @@ +#!/usr/bin/env node diff --git a/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native.cmd b/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native.cmd new file mode 100644 index 0000000000..cfa4635dea --- /dev/null +++ b/__tests__/fixtures/global/global-update-with-bin/expected-bin-files/react-native.cmd @@ -0,0 +1 @@ +@IF EXIST "%~dp0\node.exe" diff --git a/src/cli/commands/global.js b/src/cli/commands/global.js index e2ce958bd5..824433c4ce 100644 --- a/src/cli/commands/global.js +++ b/src/cli/commands/global.js @@ -1,10 +1,12 @@ /* @flow */ +import type {RegistryNames} from '../../registries/index.js'; import type {Reporter} from '../../reporters/index.js'; import type {Manifest} from '../../types.js'; import type Config from '../../config.js'; +import type PackageResolver from '../../package-resolver.js'; import {MessageError} from '../../errors.js'; -import {registries} from '../../registries/index.js'; +import {registryNames} from '../../registries/index.js'; import NoopReporter from '../../reporters/base-reporter.js'; import buildSubCommands from './_build-sub-commands.js'; import Lockfile from '../../lockfile/wrapper.js'; @@ -13,8 +15,14 @@ import {Add} from './add.js'; import {run as runRemove} from './remove.js'; import {run as runUpgrade} from './upgrade.js'; import {linkBin} from '../../package-linker.js'; +import {entries} from '../../util/misc.js'; import * as fs from '../../util/fs.js'; +type BinData = { + pkgName: string, + scriptName: string, +}; + class GlobalAdd extends Add { maybeOutputSaveTree(): Promise { for (const pattern of this.addedPatterns) { @@ -34,33 +42,52 @@ const path = require('path'); async function updateCwd(config: Config): Promise { await config.init({ cwd: config.globalFolder, - binLinks: true, + binLinks: config.binLinks, globalFolder: config.globalFolder, cacheFolder: config.cacheFolder, linkFolder: config.linkFolder, }); } -async function getBins(config: Config): Promise> { - // build up list of registry folders to search for binaries - const dirs = []; - for (const registryName of Object.keys(registries)) { - const registry = config.registries[registryName]; - dirs.push(registry.loc); +async function getBins(config: Config, resolver?: PackageResolver): Promise> { + // build up list of existing binary files + const paths: Map = new Map(); + + async function selfBinScripts(bin: Array<[string, string]>, loc: string, + pkgLoc: string, pkgName: string): Promise { + for (const [scriptName, scriptCmd] of bin) { + // Include bins that have a link in the global bin folder, + // bins are not linked when config.binLinks is false + const src = path.join(pkgLoc, scriptCmd); + const binPath = path.join(loc, '.bin', scriptName); + if (await fs.exists(binPath) && await fs.exists(src)) { + paths.set(src, {scriptName, pkgName}); + } + } } - // build up list of binary files - const paths = new Set(); - for (const dir of dirs) { - const binDir = path.join(dir, '.bin'); - if (!await fs.exists(binDir)) { - continue; + async function resolveDependencies(dependencies: Array<[string, string]>, loc: string, + registryName: RegistryNames): Promise { + for (const [name, version] of dependencies) { + const pkgLoc = path.join(loc, name); + const resolved = resolver ? + resolver.getResolvedPattern(`${name}@${version}`) : + await config.readManifest(pkgLoc, registryName); + const bin = resolved && resolved.bin || {}; + await selfBinScripts(entries(bin), loc, pkgLoc, name); } + } - for (const name of await fs.readdir(binDir)) { - paths.add(path.join(binDir, name)); - } + // build up list of registry folders to search for binaries + const rootManifests = await config.getRootManifests(); + for (const registryName of registryNames) { + const registry = config.registries[registryName]; + const manifest = rootManifests[registryName]; + // in global manifets we check only for 'dependencies' + const dependencies = manifest.exists ? manifest.object.dependencies : {}; + await resolveDependencies(entries(dependencies), registry.loc, registryName); } + return paths; } @@ -96,6 +123,13 @@ function getBinFolder(config: Config, flags: Object): string { } } +async function unlink(dest: string): Promise { + await fs.unlink(dest); + if (process.platform === 'win32' && dest.indexOf('.cmd') === -1) { + await fs.unlink(dest + '.cmd'); + } +} + async function initUpdateBins(config: Config, reporter: Reporter, flags: Object): Promise<() => Promise> { const beforeBins = await getBins(config); const binFolder = getBinFolder(config, flags); @@ -108,36 +142,38 @@ async function initUpdateBins(config: Config, reporter: Reporter, flags: Object) } } - return async function(): Promise { - const afterBins = await getBins(config); + return async function(resolver?: PackageResolver, args?: Array = []): Promise { + const afterBins = await getBins(config, resolver); // remove old bins - for (const src of beforeBins) { + for (const [src, {scriptName}] of beforeBins) { if (afterBins.has(src)) { // not old continue; } // remove old bin - const dest = path.join(binFolder, path.basename(src)); + const dest = path.join(binFolder, scriptName); try { - await fs.unlink(dest); + await unlink(dest); } catch (err) { throwPermError(err, dest); } } + const patterns = args.map((pattern) => pattern.split('@')[0]); + const pkgChanged = (pkgName: string): boolean => patterns.indexOf(pkgName) > -1; // add new bins - for (const src of afterBins) { - if (beforeBins.has(src)) { + for (const [src, {scriptName, pkgName}] of afterBins) { + if (beforeBins.has(src) && !pkgChanged(pkgName)) { // already inserted continue; } // insert new bin - const dest = path.join(binFolder, path.basename(src)); + const dest = path.join(binFolder, scriptName); try { - await fs.unlink(dest); + await unlink(dest); await linkBin(src, dest); } catch (err) { throwPermError(err, dest); @@ -182,7 +218,7 @@ const {run, setFlags: _setFlags} = buildSubCommands('global', { await install.init(); // link binaries - await updateBins(); + await updateBins(install.resolver, args); }, bin( @@ -225,10 +261,10 @@ const {run, setFlags: _setFlags} = buildSubCommands('global', { const updateBins = await initUpdateBins(config, reporter, flags); // remove module - await runRemove(config, reporter, flags, args); + const remove = await runRemove(config, reporter, flags, args); // remove binaries - await updateBins(); + await updateBins(remove.resolver); }, async upgrade( @@ -242,10 +278,10 @@ const {run, setFlags: _setFlags} = buildSubCommands('global', { const updateBins = await initUpdateBins(config, reporter, flags); // upgrade module - await runUpgrade(config, reporter, flags, args); + const upgrade = await runUpgrade(config, reporter, flags, args); // update binaries - await updateBins(); + await updateBins(upgrade.resolver, args); }, }); diff --git a/src/cli/commands/remove.js b/src/cli/commands/remove.js index 90b2aee313..0a1cc49ff3 100644 --- a/src/cli/commands/remove.js +++ b/src/cli/commands/remove.js @@ -19,7 +19,7 @@ export async function run( reporter: Reporter, flags: Object, args: Array, -): Promise { +): Promise { if (!args.length) { throw new MessageError(reporter.lang('tooFewArguments', 1)); } @@ -80,4 +80,5 @@ export async function run( // reporter.success(reporter.lang('uninstalledPackages')); + return reinstall; } diff --git a/src/cli/commands/upgrade.js b/src/cli/commands/upgrade.js index 70f0ece897..c93fa66afb 100644 --- a/src/cli/commands/upgrade.js +++ b/src/cli/commands/upgrade.js @@ -17,8 +17,9 @@ export async function run( reporter: Reporter, flags: Object, args: Array, -): Promise { +): Promise { const lockfile = args.length ? await Lockfile.fromDirectory(config.cwd, reporter) : new Lockfile(); const install = new Add(args, flags, config, reporter, lockfile); await install.init(); + return install; } diff --git a/src/package-linker.js b/src/package-linker.js index fb5a3d8219..2765c55a89 100644 --- a/src/package-linker.js +++ b/src/package-linker.js @@ -17,10 +17,12 @@ const cmdShim = promise.promisify(require('cmd-shim')); const semver = require('semver'); const path = require('path'); -type DependencyPairs = Array<{ +type DependencyPair = { dep: Manifest, loc: string -}>; +}; + +type DependencyPairs = Array; export async function linkBin(src: string, dest: string): Promise { if (process.platform === 'win32') { @@ -43,6 +45,11 @@ export default class PackageLinker { resolver: PackageResolver; config: Config; + // make linkBin testable by inserting the function into PackageLinker + async linkBin(src: string, dest: string): Promise { + await linkBin(src, dest); + } + async linkSelfDependencies(pkg: Manifest, pkgLoc: string, targetBinLoc: string): Promise { targetBinLoc = await fs.realpath(targetBinLoc); pkgLoc = await fs.realpath(pkgLoc); @@ -53,11 +60,11 @@ export default class PackageLinker { // TODO maybe throw an error continue; } - await linkBin(src, dest); + await this.linkBin(src, dest); } } - async linkBinDependencies(pkg: Manifest, dir: string): Promise { + async getBinDependencies(pkg: Manifest): Promise { const deps: DependencyPairs = []; const ref = pkg._reference; @@ -91,18 +98,68 @@ export default class PackageLinker { } } - // no deps to link - if (!deps.length) { - return; + return deps; + } + + async linkDependencies(flatTree: HoistManifestTuples): Promise { + // Create a map of .bin location to a map of bin command and its + // source location. we remove all duplicates for each .bin location, + // this will prevents multiple calls to create the same symlink. + const binsByLocation: Map> = new Map(); + async function getBinLinks(binLoc: string): Promise> { + if (!binsByLocation.has(binLoc)) { + // ensure our .bin file we're writing these to exists + await fs.mkdirp(binLoc); + binsByLocation.set(binLoc, new Map()); + } + const binLinks = binsByLocation.get(binLoc); + invariant(binLinks, 'expected value'); + return binLinks; } - // ensure our .bin file we're writing these to exists - const binLoc = path.join(dir, '.bin'); - await fs.mkdirp(binLoc); + let binsCount = 0; + for (const [dest, {pkg}] of flatTree) { + const modules = this.config.getFolder(pkg); + const rootLoc = path.join(this.config.cwd, modules); + const pkgLoc = path.dirname(dest); + const parentBinLoc = path.join(pkgLoc, '.bin'); + const pkgBinLoc = path.join(dest, modules, '.bin'); + + const deps = await this.getBinDependencies(pkg); + + for (const {dep, loc} of deps) { + // replace dependency location that point to different package, + // if the one in the current location is identical + let newDepLoc; + const depLoc = path.dirname(loc); + if (depLoc !== pkgLoc && depLoc !== rootLoc && path.dirname(depLoc) !== dest) { + newDepLoc = path.join(dest, modules, dep.name); + const manifest = await this.config.maybeReadManifest(newDepLoc); + if (!manifest || manifest.version !== dep.version) { + newDepLoc = null; + } + } + + // When both package and dependency are in the same folder use the .bin + // in that folder, else use the .bin in the package. + const location = newDepLoc || loc; + const binLoc = path.dirname(location) === pkgLoc ? parentBinLoc : pkgBinLoc; + const binLinks = await getBinLinks(binLoc); + // Remove Duplicates + if (!binLinks.has(location)) { + binLinks.set(location, dep); + binsCount++; + } + } + } // write the executables - for (const {dep, loc} of deps) { - await this.linkSelfDependencies(dep, loc, binLoc); + const tickBin = this.reporter.progress(binsCount); + for (const [binLoc, binLinks] of binsByLocation) { + for (const [loc, dep] of binLinks) { + await this.linkSelfDependencies(dep, loc, binLoc); + tickBin(loc); + } } } @@ -202,12 +259,7 @@ export default class PackageLinker { // if (this.config.binLinks) { - const tickBin = this.reporter.progress(flatTree.length); - await promise.queue(flatTree, async ([dest, {pkg}]) => { - const binLoc = path.join(dest, this.config.getFolder(pkg)); - await this.linkBinDependencies(pkg, binLoc); - tickBin(dest); - }, 4); + await this.linkDependencies(flatTree); } }