Skip to content

Commit b1f667b

Browse files
Timerrandycoulman
authored andcommitted
Properly extract package name for installing tgz of scoped packages (facebook#1706)
* Properly extract package name * Download package if need be ... * Oops * Add e2e test based on facebook#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
1 parent e18e4ff commit b1f667b

File tree

3 files changed

+104
-20
lines changed

3 files changed

+104
-20
lines changed

packages/create-react-app/index.js

+89-20
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ var execSync = require('child_process').execSync;
6060
var spawn = require('cross-spawn');
6161
var semver = require('semver');
6262
var dns = require('dns');
63+
var tmp = require('tmp');
64+
var unpack = require('tar-pack').unpack;
65+
var hyperquest = require('hyperquest');
6366

6467
var projectName;
6568

@@ -201,23 +204,34 @@ function install(useYarn, dependencies, verbose, isOnline) {
201204

202205
function run(root, appName, version, verbose, originalDirectory, template) {
203206
var packageToInstall = getInstallPackage(version);
204-
var packageName = getPackageName(packageToInstall);
205-
206207
var allDependencies = ['react', 'react-dom', packageToInstall];
207208

208209
console.log('Installing packages. This might take a couple minutes.');
209-
console.log(
210-
'Installing ' + chalk.cyan('react') + ', ' + chalk.cyan('react-dom') +
211-
', and ' + chalk.cyan(packageName) + '...'
212-
);
213-
console.log();
214-
210+
215211
var useYarn = shouldUseYarn();
216-
checkIfOnline(useYarn)
217-
.then(function(isOnline) {
218-
return install(useYarn, allDependencies, verbose, isOnline);
212+
getPackageName(packageToInstall)
213+
.then(function(packageName) {
214+
return checkIfOnline(useYarn).then(function(isOnline) {
215+
return {
216+
isOnline: isOnline,
217+
packageName: packageName,
218+
};
219+
});
220+
})
221+
.then(function(info) {
222+
var isOnline = info.isOnline;
223+
var packageName = info.packageName;
224+
console.log(
225+
'Installing ' + chalk.cyan('react') + ', ' + chalk.cyan('react-dom') +
226+
', and ' + chalk.cyan(packageName) + '...'
227+
);
228+
console.log();
229+
230+
return install(useYarn, allDependencies, verbose, isOnline).then(function() {
231+
return packageName;
232+
});
219233
})
220-
.then(function() {
234+
.then(function(packageName) {
221235
checkNodeVersion(packageName);
222236

223237
// Since react-scripts has been installed with --save
@@ -285,22 +299,77 @@ function getInstallPackage(version) {
285299
return packageToInstall;
286300
}
287301

302+
function getTemporaryDirectory() {
303+
return new Promise(function(resolve, reject) {
304+
// Unsafe cleanup lets us recursively delete the directory if it contains
305+
// contents; by default it only allows removal if it's empty
306+
tmp.dir({ unsafeCleanup: true }, function(err, tmpdir, callback) {
307+
if (err) {
308+
reject(err);
309+
} else {
310+
resolve({
311+
tmpdir: tmpdir,
312+
cleanup: function() {
313+
try {
314+
callback();
315+
} catch (ignored) {
316+
// Callback might throw and fail, since it's a temp directory the
317+
// OS will clean it up eventually...
318+
}
319+
}
320+
});
321+
}
322+
});
323+
});
324+
}
325+
326+
function extractStream(stream, dest) {
327+
return new Promise(function(resolve, reject) {
328+
stream.pipe(unpack(dest, function(err) {
329+
if (err) {
330+
reject(err);
331+
} else {
332+
resolve(dest);
333+
}
334+
}));
335+
});
336+
}
337+
288338
// Extract package name from tarball url or path.
289339
function getPackageName(installPackage) {
290340
if (installPackage.indexOf('.tgz') > -1) {
291-
// The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
292-
// However, this function returns package name only without semver version.
293-
return installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.tgz$/)[1];
341+
return getTemporaryDirectory().then(function(obj) {
342+
var stream;
343+
if (/^http/.test(installPackage)) {
344+
stream = hyperquest(installPackage);
345+
} else {
346+
stream = fs.createReadStream(installPackage);
347+
}
348+
return extractStream(stream, obj.tmpdir).then(function() {
349+
return obj;
350+
});
351+
}).then(function(obj) {
352+
var packageName = require(path.join(obj.tmpdir, 'package.json')).name;
353+
obj.cleanup();
354+
return packageName;
355+
}).catch(function(err) {
356+
// The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
357+
// However, this function returns package name only without semver version.
358+
console.log('Could not extract the package name from the archive: ' + err.message);
359+
var assumedProjectName = installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.tgz$/)[1];
360+
console.log('Based on the filename, assuming it is "' + chalk.cyan(assumedProjectName) + '"');
361+
return Promise.resolve(assumedProjectName);
362+
});
294363
} else if (installPackage.indexOf('git+') === 0) {
295364
// Pull package name out of git urls e.g:
296365
// git+https://github.com/mycompany/react-scripts.git
297366
// git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
298-
return installPackage.match(/([^\/]+)\.git(#.*)?$/)[1];
367+
return Promise.resolve(installPackage.match(/([^\/]+)\.git(#.*)?$/)[1]);
299368
} else if (installPackage.indexOf('@') > 0) {
300369
// Do not match @scope/ when stripping off @version or @tag
301-
return installPackage.charAt(0) + installPackage.substr(1).split('@')[0];
370+
return Promise.resolve(installPackage.charAt(0) + installPackage.substr(1).split('@')[0]);
302371
}
303-
return installPackage;
372+
return Promise.resolve(installPackage);
304373
}
305374

306375
function checkNpmVersion() {
@@ -356,7 +425,7 @@ function checkAppName(appName) {
356425
printValidationResults(validationResult.warnings);
357426
process.exit(1);
358427
}
359-
428+
360429
// TODO: there should be a single place that holds the dependencies
361430
var dependencies = ['react', 'react-dom'];
362431
var devDependencies = ['react-scripts'];
@@ -449,7 +518,7 @@ function checkIfOnline(useYarn) {
449518
// We'll just assume the best case.
450519
return Promise.resolve(true);
451520
}
452-
521+
453522
return new Promise(function(resolve) {
454523
dns.resolve('registry.yarnpkg.com', function(err) {
455524
resolve(err === null);

packages/create-react-app/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
"commander": "^2.9.0",
2525
"cross-spawn": "^4.0.0",
2626
"fs-extra": "^1.0.0",
27+
"hyperquest": "^2.1.2",
2728
"semver": "^5.0.3",
29+
"tar-pack": "^3.4.0",
30+
"tmp": "0.0.31",
2831
"validate-npm-package-name": "^3.0.0"
2932
}
3033
}

tasks/e2e-installs.sh

+12
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ if [ "$(ls -1 ./test-app-should-remain | wc -l | tr -d '[:space:]')" != "1" ]; t
145145
false
146146
fi
147147

148+
# ******************************************************************************
149+
# Test --scripts-version with a scoped fork tgz of react-scripts
150+
# ******************************************************************************
151+
152+
cd $temp_app_path
153+
curl "https://registry.npmjs.org/@enoah_netzach/react-scripts/-/react-scripts-0.9.0.tgz" -o enoah-scripts-0.9.0.tgz
154+
create_react_app --scripts-version=$temp_app_path/enoah-scripts-0.9.0.tgz test-app-scoped-fork-tgz
155+
cd test-app-scoped-fork-tgz
156+
157+
# Check corresponding scripts version is installed.
158+
exists node_modules/@enoah_netzach/react-scripts
159+
148160
# ******************************************************************************
149161
# Test nested folder path as the project name
150162
# ******************************************************************************

0 commit comments

Comments
 (0)