From 50368252ed40b2838373c4007fc841ac17b950ef Mon Sep 17 00:00:00 2001 From: Ian Sutherland Date: Wed, 5 Aug 2020 11:25:10 -0600 Subject: [PATCH] Fix template name handling (#9412) --- packages/create-react-app/__tests__/.eslintrc | 5 + .../getTemplateInstallPackage.test.js | 82 +++++ packages/create-react-app/createReactApp.js | 348 +++++++++--------- packages/create-react-app/index.js | 4 +- packages/create-react-app/package.json | 7 + 5 files changed, 280 insertions(+), 166 deletions(-) create mode 100644 packages/create-react-app/__tests__/.eslintrc create mode 100644 packages/create-react-app/__tests__/getTemplateInstallPackage.test.js diff --git a/packages/create-react-app/__tests__/.eslintrc b/packages/create-react-app/__tests__/.eslintrc new file mode 100644 index 00000000000..55f121d152d --- /dev/null +++ b/packages/create-react-app/__tests__/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "jest": true + } +} diff --git a/packages/create-react-app/__tests__/getTemplateInstallPackage.test.js b/packages/create-react-app/__tests__/getTemplateInstallPackage.test.js new file mode 100644 index 00000000000..457c906b5bb --- /dev/null +++ b/packages/create-react-app/__tests__/getTemplateInstallPackage.test.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const { getTemplateInstallPackage } = require('../createReactApp'); + +describe('getTemplateInstallPackage', () => { + it('no options gives cra-template', async () => { + await expect(getTemplateInstallPackage()).resolves.toBe('cra-template'); + }); + + it('cra-template gives cra-template', async () => { + await expect(getTemplateInstallPackage('cra-template')).resolves.toBe( + 'cra-template' + ); + }); + + it('cra-template-typescript gives cra-template-typescript', async () => { + await expect(getTemplateInstallPackage('cra-template-typescript')).resolves.toBe( + 'cra-template-typescript' + ); + }); + + it('typescript gives cra-template-typescript', async () => { + await expect(getTemplateInstallPackage('typescript')).resolves.toBe( + 'cra-template-typescript' + ); + }); + + it('typescript@next gives cra-template-typescript@next', async () => { + await expect(getTemplateInstallPackage('cra-template-typescript@next')).resolves.toBe( + 'cra-template-typescript@next' + ); + }); + + it('cra-template@next gives cra-template@next', async () => { + await expect(getTemplateInstallPackage('cra-template@next')).resolves.toBe( + 'cra-template@next' + ); + }); + + it('cra-template-typescript@next gives cra-template-typescript@next', async () => { + await expect(getTemplateInstallPackage('cra-template-typescript@next')).resolves.toBe( + 'cra-template-typescript@next' + ); + }); + + it('@iansu gives @iansu/cra-template', async () => { + await expect(getTemplateInstallPackage('@iansu')).resolves.toBe( + '@iansu/cra-template' + ); + }); + + it('@iansu/cra-template gives @iansu/cra-template', async () => { + await expect( + getTemplateInstallPackage('@iansu/cra-template') + ).resolves.toBe('@iansu/cra-template'); + }); + + it('@iansu/cra-template@next gives @iansu/cra-template@next', async () => { + await expect( + getTemplateInstallPackage('@iansu/cra-template@next') + ).resolves.toBe('@iansu/cra-template@next'); + }); + + it('@iansu/cra-template-typescript@next gives @iansu/cra-template-typescript@next', async () => { + await expect(getTemplateInstallPackage('@iansu/cra-template-typescript@next')).resolves.toBe( + '@iansu/cra-template-typescript@next' + ); + }); + + it('http://example.com/cra-template.tar.gz gives http://example.com/cra-template.tar.gz', async () => { + await expect( + getTemplateInstallPackage('http://example.com/cra-template.tar.gz') + ).resolves.toBe('http://example.com/cra-template.tar.gz'); + }); +}); diff --git a/packages/create-react-app/createReactApp.js b/packages/create-react-app/createReactApp.js index ae13d1eb600..aac5b33da6a 100755 --- a/packages/create-react-app/createReactApp.js +++ b/packages/create-react-app/createReactApp.js @@ -51,178 +51,190 @@ const packageJson = require('./package.json'); let projectName; -const program = new commander.Command(packageJson.name) - .version(packageJson.version) - .arguments('') - .usage(`${chalk.green('')} [options]`) - .action(name => { - projectName = name; - }) - .option('--verbose', 'print additional logs') - .option('--info', 'print environment debug info') - .option( - '--scripts-version ', - 'use a non-standard version of react-scripts' - ) - .option( - '--template ', - 'specify a template for the created project' - ) - .option('--use-npm') - .option('--use-pnp') - .allowUnknownOption() - .on('--help', () => { - console.log(` Only ${chalk.green('')} is required.`); - console.log(); - console.log( - ` A custom ${chalk.cyan('--scripts-version')} can be one of:` - ); - console.log(` - a specific npm version: ${chalk.green('0.8.2')}`); - console.log(` - a specific npm tag: ${chalk.green('@next')}`); - console.log( - ` - a custom fork published on npm: ${chalk.green( - 'my-react-scripts' - )}` - ); - console.log( - ` - a local path relative to the current working directory: ${chalk.green( - 'file:../my-react-scripts' - )}` - ); - console.log( - ` - a .tgz archive: ${chalk.green( - 'https://mysite.com/my-react-scripts-0.8.2.tgz' - )}` - ); +function init() { + const program = new commander.Command(packageJson.name) + .version(packageJson.version) + .arguments('') + .usage(`${chalk.green('')} [options]`) + .action(name => { + projectName = name; + }) + .option('--verbose', 'print additional logs') + .option('--info', 'print environment debug info') + .option( + '--scripts-version ', + 'use a non-standard version of react-scripts' + ) + .option( + '--template ', + 'specify a template for the created project' + ) + .option('--use-npm') + .option('--use-pnp') + .allowUnknownOption() + .on('--help', () => { + console.log( + ` Only ${chalk.green('')} is required.` + ); + console.log(); + console.log( + ` A custom ${chalk.cyan('--scripts-version')} can be one of:` + ); + console.log(` - a specific npm version: ${chalk.green('0.8.2')}`); + console.log(` - a specific npm tag: ${chalk.green('@next')}`); + console.log( + ` - a custom fork published on npm: ${chalk.green( + 'my-react-scripts' + )}` + ); + console.log( + ` - a local path relative to the current working directory: ${chalk.green( + 'file:../my-react-scripts' + )}` + ); + console.log( + ` - a .tgz archive: ${chalk.green( + 'https://mysite.com/my-react-scripts-0.8.2.tgz' + )}` + ); + console.log( + ` - a .tar.gz archive: ${chalk.green( + 'https://mysite.com/my-react-scripts-0.8.2.tar.gz' + )}` + ); + console.log( + ` It is not needed unless you specifically want to use a fork.` + ); + console.log(); + console.log(` A custom ${chalk.cyan('--template')} can be one of:`); + console.log( + ` - a custom template published on npm: ${chalk.green( + 'cra-template-typescript' + )}` + ); + console.log( + ` - a local path relative to the current working directory: ${chalk.green( + 'file:../my-custom-template' + )}` + ); + console.log( + ` - a .tgz archive: ${chalk.green( + 'https://mysite.com/my-custom-template-0.8.2.tgz' + )}` + ); + console.log( + ` - a .tar.gz archive: ${chalk.green( + 'https://mysite.com/my-custom-template-0.8.2.tar.gz' + )}` + ); + console.log(); + console.log( + ` If you have any problems, do not hesitate to file an issue:` + ); + console.log( + ` ${chalk.cyan( + 'https://github.com/facebook/create-react-app/issues/new' + )}` + ); + console.log(); + }) + .parse(process.argv); + + if (program.info) { + console.log(chalk.bold('\nEnvironment Info:')); console.log( - ` - a .tar.gz archive: ${chalk.green( - 'https://mysite.com/my-react-scripts-0.8.2.tar.gz' - )}` + `\n current version of ${packageJson.name}: ${packageJson.version}` ); + console.log(` running from ${__dirname}`); + return envinfo + .run( + { + System: ['OS', 'CPU'], + Binaries: ['Node', 'npm', 'Yarn'], + Browsers: [ + 'Chrome', + 'Edge', + 'Internet Explorer', + 'Firefox', + 'Safari', + ], + npmPackages: ['react', 'react-dom', 'react-scripts'], + npmGlobalPackages: ['create-react-app'], + }, + { + duplicates: true, + showNotFound: true, + } + ) + .then(console.log); + } + + if (typeof projectName === 'undefined') { + console.error('Please specify the project directory:'); console.log( - ` It is not needed unless you specifically want to use a fork.` + ` ${chalk.cyan(program.name())} ${chalk.green('')}` ); console.log(); - console.log(` A custom ${chalk.cyan('--template')} can be one of:`); - console.log( - ` - a custom template published on npm: ${chalk.green( - 'cra-template-typescript' - )}` - ); - console.log( - ` - a local path relative to the current working directory: ${chalk.green( - 'file:../my-custom-template' - )}` - ); - console.log( - ` - a .tgz archive: ${chalk.green( - 'https://mysite.com/my-custom-template-0.8.2.tgz' - )}` - ); + console.log('For example:'); console.log( - ` - a .tar.gz archive: ${chalk.green( - 'https://mysite.com/my-custom-template-0.8.2.tar.gz' - )}` + ` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}` ); console.log(); console.log( - ` If you have any problems, do not hesitate to file an issue:` + `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` ); - console.log( - ` ${chalk.cyan( - 'https://github.com/facebook/create-react-app/issues/new' - )}` - ); - console.log(); - }) - .parse(process.argv); + process.exit(1); + } -if (program.info) { - console.log(chalk.bold('\nEnvironment Info:')); - console.log( - `\n current version of ${packageJson.name}: ${packageJson.version}` - ); - console.log(` running from ${__dirname}`); - return envinfo - .run( - { - System: ['OS', 'CPU'], - Binaries: ['Node', 'npm', 'Yarn'], - Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'], - npmPackages: ['react', 'react-dom', 'react-scripts'], - npmGlobalPackages: ['create-react-app'], - }, - { - duplicates: true, - showNotFound: true, + // We first check the registry directly via the API, and if that fails, we try + // the slower `npm view [package] version` command. + // + // This is important for users in environments where direct access to npm is + // blocked by a firewall, and packages are provided exclusively via a private + // registry. + checkForLatestVersion() + .catch(() => { + try { + return execSync('npm view create-react-app version').toString().trim(); + } catch (e) { + return null; } - ) - .then(console.log); -} - -if (typeof projectName === 'undefined') { - console.error('Please specify the project directory:'); - console.log( - ` ${chalk.cyan(program.name())} ${chalk.green('')}` - ); - console.log(); - console.log('For example:'); - console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`); - console.log(); - console.log( - `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` - ); - process.exit(1); + }) + .then(latest => { + if (latest && semver.lt(packageJson.version, latest)) { + console.log(); + console.error( + chalk.yellow( + `You are running \`create-react-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` + + 'We no longer support global installation of Create React App.' + ) + ); + console.log(); + console.log( + 'Please remove any global installs with one of the following commands:\n' + + '- npm uninstall -g create-react-app\n' + + '- yarn global remove create-react-app' + ); + console.log(); + console.log( + 'The latest instructions for creating a new app can be found here:\n' + + 'https://create-react-app.dev/docs/getting-started/' + ); + console.log(); + process.exit(1); + } else { + createApp( + projectName, + program.verbose, + program.scriptsVersion, + program.template, + program.useNpm, + program.usePnp + ); + } + }); } -// We first check the registry directly via the API, and if that fails, we try -// the slower `npm view [package] version` command. -// -// This is important for users in environments where direct access to npm is -// blocked by a firewall, and packages are provided exclusively via a private -// registry. -checkForLatestVersion() - .catch(() => { - try { - return execSync('npm view create-react-app version').toString().trim(); - } catch (e) { - return null; - } - }) - .then(latest => { - if (latest && semver.lt(packageJson.version, latest)) { - console.log(); - console.error( - chalk.yellow( - `You are running \`create-react-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` + - 'We no longer support global installation of Create React App.' - ) - ); - console.log(); - console.log( - 'Please remove any global installs with one of the following commands:\n' + - '- npm uninstall -g create-react-app\n' + - '- yarn global remove create-react-app' - ); - console.log(); - console.log( - 'The latest instructions for creating a new app can be found here:\n' + - 'https://create-react-app.dev/docs/getting-started/' - ); - console.log(); - process.exit(1); - } else { - createApp( - projectName, - program.verbose, - program.scriptsVersion, - program.template, - program.useNpm, - program.usePnp - ); - } - }); - function createApp(name, verbose, version, template, useNpm, usePnp) { const unsupportedNodeVersion = !semver.satisfies(process.version, '>=10'); if (unsupportedNodeVersion) { @@ -628,10 +640,11 @@ function getTemplateInstallPackage(template, originalDirectory) { templateToInstall = template; } else { // Add prefix 'cra-template-' to non-prefixed templates, leaving any - // @scope/ intact. - const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/); + // @scope/ and @version intact. + const packageMatch = template.match(/^(@[^/]+\/)?([^@]+)?(@.+)?$/); const scope = packageMatch[1] || ''; - const templateName = packageMatch[2]; + const templateName = packageMatch[2] || ''; + const version = packageMatch[3] || ''; if ( templateName === templateToInstall || @@ -642,15 +655,15 @@ function getTemplateInstallPackage(template, originalDirectory) { // - @SCOPE/cra-template // - cra-template-NAME // - @SCOPE/cra-template-NAME - templateToInstall = `${scope}${templateName}`; - } else if (templateName.startsWith('@')) { + templateToInstall = `${scope}${templateName}${version}`; + } else if (version && !scope && !templateName) { // Covers using @SCOPE only - templateToInstall = `${templateName}/${templateToInstall}`; + templateToInstall = `${version}/${templateToInstall}`; } else { // Covers templates without the `cra-template` prefix: // - NAME // - @SCOPE/NAME - templateToInstall = `${scope}${templateToInstall}-${templateName}`; + templateToInstall = `${scope}${templateToInstall}-${templateName}${version}`; } } } @@ -1133,3 +1146,8 @@ function checkForLatestVersion() { }); }); } + +module.exports = { + init, + getTemplateInstallPackage, +}; diff --git a/packages/create-react-app/index.js b/packages/create-react-app/index.js index 1e068819f48..e8e418b10da 100755 --- a/packages/create-react-app/index.js +++ b/packages/create-react-app/index.js @@ -51,4 +51,6 @@ if (major < 10) { process.exit(1); } -require('./createReactApp'); +const { init } = require('./createReactApp'); + +init(); diff --git a/packages/create-react-app/package.json b/packages/create-react-app/package.json index 013968b725d..3d87484b2b0 100644 --- a/packages/create-react-app/package.json +++ b/packages/create-react-app/package.json @@ -25,6 +25,9 @@ "bin": { "create-react-app": "./index.js" }, + "scripts": { + "test": "cross-env FORCE_COLOR=true jest" + }, "dependencies": { "chalk": "4.1.0", "commander": "4.1.0", @@ -37,5 +40,9 @@ "tar-pack": "3.4.1", "tmp": "0.2.1", "validate-npm-package-name": "3.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.2", + "jest": "26.1.0" } }