From 3fa5e8e7d7a1b0d004e51a39afc2aa074808aa2a Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 5 Mar 2017 18:45:29 -0500 Subject: [PATCH] Properly extract package name for installing tgz of scoped packages (#1706) * Properly extract package name * Download package if need be ... * Oops * Add e2e test based on #1537, but without specific filename * Pass packageName through promises A little bit more verbose but explicit and doesn't rely on shared mutable state. * Fix up directory name in test * Tweak failure message * Fix lint --- packages/create-react-app/index.js | 109 ++++++++++++++++++++----- packages/create-react-app/package.json | 3 + tasks/e2e-installs.sh | 12 +++ 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/packages/create-react-app/index.js b/packages/create-react-app/index.js index 5c2205dc6fc..ebeae5b41b0 100755 --- a/packages/create-react-app/index.js +++ b/packages/create-react-app/index.js @@ -60,6 +60,9 @@ var execSync = require('child_process').execSync; var spawn = require('cross-spawn'); var semver = require('semver'); var dns = require('dns'); +var tmp = require('tmp'); +var unpack = require('tar-pack').unpack; +var hyperquest = require('hyperquest'); var projectName; @@ -201,23 +204,34 @@ function install(useYarn, dependencies, verbose, isOnline) { function run(root, appName, version, verbose, originalDirectory, template) { var packageToInstall = getInstallPackage(version); - var packageName = getPackageName(packageToInstall); - var allDependencies = ['react', 'react-dom', packageToInstall]; console.log('Installing packages. This might take a couple minutes.'); - console.log( - 'Installing ' + chalk.cyan('react') + ', ' + chalk.cyan('react-dom') + - ', and ' + chalk.cyan(packageName) + '...' - ); - console.log(); - + var useYarn = shouldUseYarn(); - checkIfOnline(useYarn) - .then(function(isOnline) { - return install(useYarn, allDependencies, verbose, isOnline); + getPackageName(packageToInstall) + .then(function(packageName) { + return checkIfOnline(useYarn).then(function(isOnline) { + return { + isOnline: isOnline, + packageName: packageName, + }; + }); + }) + .then(function(info) { + var isOnline = info.isOnline; + var packageName = info.packageName; + console.log( + 'Installing ' + chalk.cyan('react') + ', ' + chalk.cyan('react-dom') + + ', and ' + chalk.cyan(packageName) + '...' + ); + console.log(); + + return install(useYarn, allDependencies, verbose, isOnline).then(function() { + return packageName; + }); }) - .then(function() { + .then(function(packageName) { checkNodeVersion(packageName); // Since react-scripts has been installed with --save @@ -285,22 +299,77 @@ function getInstallPackage(version) { return packageToInstall; } +function getTemporaryDirectory() { + return new Promise(function(resolve, reject) { + // Unsafe cleanup lets us recursively delete the directory if it contains + // contents; by default it only allows removal if it's empty + tmp.dir({ unsafeCleanup: true }, function(err, tmpdir, callback) { + if (err) { + reject(err); + } else { + resolve({ + tmpdir: tmpdir, + cleanup: function() { + try { + callback(); + } catch (ignored) { + // Callback might throw and fail, since it's a temp directory the + // OS will clean it up eventually... + } + } + }); + } + }); + }); +} + +function extractStream(stream, dest) { + return new Promise(function(resolve, reject) { + stream.pipe(unpack(dest, function(err) { + if (err) { + reject(err); + } else { + resolve(dest); + } + })); + }); +} + // Extract package name from tarball url or path. function getPackageName(installPackage) { if (installPackage.indexOf('.tgz') > -1) { - // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz - // However, this function returns package name only without semver version. - return installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.tgz$/)[1]; + return getTemporaryDirectory().then(function(obj) { + var stream; + if (/^http/.test(installPackage)) { + stream = hyperquest(installPackage); + } else { + stream = fs.createReadStream(installPackage); + } + return extractStream(stream, obj.tmpdir).then(function() { + return obj; + }); + }).then(function(obj) { + var packageName = require(path.join(obj.tmpdir, 'package.json')).name; + obj.cleanup(); + return packageName; + }).catch(function(err) { + // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz + // However, this function returns package name only without semver version. + console.log('Could not extract the package name from the archive: ' + err.message); + var assumedProjectName = installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.tgz$/)[1]; + console.log('Based on the filename, assuming it is "' + chalk.cyan(assumedProjectName) + '"'); + return Promise.resolve(assumedProjectName); + }); } else if (installPackage.indexOf('git+') === 0) { // Pull package name out of git urls e.g: // git+https://github.com/mycompany/react-scripts.git // git+ssh://github.com/mycompany/react-scripts.git#v1.2.3 - return installPackage.match(/([^\/]+)\.git(#.*)?$/)[1]; + return Promise.resolve(installPackage.match(/([^\/]+)\.git(#.*)?$/)[1]); } else if (installPackage.indexOf('@') > 0) { // Do not match @scope/ when stripping off @version or @tag - return installPackage.charAt(0) + installPackage.substr(1).split('@')[0]; + return Promise.resolve(installPackage.charAt(0) + installPackage.substr(1).split('@')[0]); } - return installPackage; + return Promise.resolve(installPackage); } function checkNpmVersion() { @@ -356,7 +425,7 @@ function checkAppName(appName) { printValidationResults(validationResult.warnings); process.exit(1); } - + // TODO: there should be a single place that holds the dependencies var dependencies = ['react', 'react-dom']; var devDependencies = ['react-scripts']; @@ -449,7 +518,7 @@ function checkIfOnline(useYarn) { // We'll just assume the best case. return Promise.resolve(true); } - + return new Promise(function(resolve) { dns.resolve('registry.yarnpkg.com', function(err) { resolve(err === null); diff --git a/packages/create-react-app/package.json b/packages/create-react-app/package.json index add92d005ca..8752f50273b 100644 --- a/packages/create-react-app/package.json +++ b/packages/create-react-app/package.json @@ -24,7 +24,10 @@ "commander": "^2.9.0", "cross-spawn": "^4.0.0", "fs-extra": "^1.0.0", + "hyperquest": "^2.1.2", "semver": "^5.0.3", + "tar-pack": "^3.4.0", + "tmp": "0.0.31", "validate-npm-package-name": "^3.0.0" } } diff --git a/tasks/e2e-installs.sh b/tasks/e2e-installs.sh index 35ec3521b4c..cacf4f96e1f 100755 --- a/tasks/e2e-installs.sh +++ b/tasks/e2e-installs.sh @@ -145,6 +145,18 @@ if [ "$(ls -1 ./test-app-should-remain | wc -l | tr -d '[:space:]')" != "1" ]; t false fi +# ****************************************************************************** +# Test --scripts-version with a scoped fork tgz of react-scripts +# ****************************************************************************** + +cd $temp_app_path +curl "https://registry.npmjs.org/@enoah_netzach/react-scripts/-/react-scripts-0.9.0.tgz" -o enoah-scripts-0.9.0.tgz +create_react_app --scripts-version=$temp_app_path/enoah-scripts-0.9.0.tgz test-app-scoped-fork-tgz +cd test-app-scoped-fork-tgz + +# Check corresponding scripts version is installed. +exists node_modules/@enoah_netzach/react-scripts + # ****************************************************************************** # Test nested folder path as the project name # ******************************************************************************