From 311667b12e504e0f808220aa690710305c14bc7a Mon Sep 17 00:00:00 2001 From: macjabeth Date: Mon, 7 Oct 2019 01:06:22 -0400 Subject: [PATCH] Add support for pnpm package manager --- docusaurus/docs/getting-started.md | 2 + packages/create-react-app/createReactApp.js | 44 ++++++++++++++----- packages/react-dev-utils/README.md | 5 ++- .../react-dev-utils/WebpackDevServerUtils.js | 9 ++-- .../printHostingInstructions.js | 27 ++++++++---- packages/react-scripts/config/paths.js | 1 + packages/react-scripts/scripts/build.js | 4 +- packages/react-scripts/scripts/init.js | 5 ++- packages/react-scripts/scripts/start.js | 2 + tasks/e2e-installs.sh | 14 ++++++ 10 files changed, 84 insertions(+), 29 deletions(-) diff --git a/docusaurus/docs/getting-started.md b/docusaurus/docs/getting-started.md index 9831e638a41..879db9d6db8 100644 --- a/docusaurus/docs/getting-started.md +++ b/docusaurus/docs/getting-started.md @@ -74,6 +74,8 @@ When you create a new app, the CLI will use [Yarn](https://yarnpkg.com/) to inst npx create-react-app my-app --use-npm ``` +You may also opt to use pnpm with `--use-pnpm`. + ## Output Running any of these commands will create a directory called `my-app` inside the current folder. Inside that directory, it will generate the initial project structure and install the transitive dependencies: diff --git a/packages/create-react-app/createReactApp.js b/packages/create-react-app/createReactApp.js index 5a377d278e6..e88e43b0d5d 100755 --- a/packages/create-react-app/createReactApp.js +++ b/packages/create-react-app/createReactApp.js @@ -77,6 +77,7 @@ const program = new commander.Command(packageJson.name) 'use a non-standard version of react-scripts' ) .option('--use-npm') + .option('--use-pnpm') .option('--use-pnp') .option('--typescript') .allowUnknownOption() @@ -179,6 +180,7 @@ createApp( program.verbose, program.scriptsVersion, program.useNpm, + program.usePnpm, program.usePnp, program.typescript, hiddenProgram.internalTestingTemplate @@ -189,6 +191,7 @@ function createApp( verbose, version, useNpm, + usePnpm, usePnp, useTypescript, template @@ -215,10 +218,10 @@ function createApp( JSON.stringify(packageJson, null, 2) + os.EOL ); - const useYarn = useNpm ? false : shouldUseYarn(); + const useYarn = useNpm || usePnpm ? false : shouldUseYarn(); const originalDirectory = process.cwd(); process.chdir(root); - if (!useYarn && !checkThatNpmCanReadCwd()) { + if (!useYarn && !checkThatNpmCanReadCwd(usePnpm)) { process.exit(1); } @@ -234,12 +237,14 @@ function createApp( } if (!useYarn) { - const npmInfo = checkNpmVersion(); + const npmInfo = checkNpmVersion(usePnpm); if (!npmInfo.hasMinNpm) { if (npmInfo.npmVersion) { console.log( chalk.yellow( - `You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` + + `You are using ${usePnpm ? 'pnpm' : 'npm'} ${ + npmInfo.npmVersion + } so the project will be bootstrapped with an old unsupported version of tools.\n\n` + `Please update to npm 5 or higher for a better, fully supported experience.\n` ) ); @@ -289,6 +294,7 @@ function createApp( originalDirectory, template, useYarn, + usePnpm, usePnp, useTypescript ); @@ -303,7 +309,15 @@ function shouldUseYarn() { } } -function install(root, useYarn, usePnp, dependencies, verbose, isOnline) { +function install( + root, + useYarn, + usePnpm, + usePnp, + dependencies, + verbose, + isOnline +) { return new Promise((resolve, reject) => { let command; let args; @@ -332,7 +346,7 @@ function install(root, useYarn, usePnp, dependencies, verbose, isOnline) { console.log(); } } else { - command = 'npm'; + command = usePnpm ? 'pnpm' : 'npm'; args = [ 'install', '--save', @@ -342,7 +356,9 @@ function install(root, useYarn, usePnp, dependencies, verbose, isOnline) { ].concat(dependencies); if (usePnp) { - console.log(chalk.yellow("NPM doesn't support PnP.")); + console.log( + chalk.yellow(`${usePnpm ? 'PNPM' : 'NPM'} doesn't support PnP.`) + ); console.log(chalk.yellow('Falling back to the regular installs.')); console.log(); } @@ -373,6 +389,7 @@ function run( originalDirectory, template, useYarn, + usePnpm, usePnp, useTypescript ) { @@ -411,6 +428,7 @@ function run( return install( root, useYarn, + usePnpm, usePnp, allDependencies, verbose, @@ -635,14 +653,14 @@ function getPackageName(installPackage) { return Promise.resolve(installPackage); } -function checkNpmVersion() { +function checkNpmVersion(usePnpm) { let hasMinNpm = false; let npmVersion = null; try { - npmVersion = execSync('npm --version') + npmVersion = execSync(`${usePnpm ? 'pnpm' : 'npm'} --version`) .toString() .trim(); - hasMinNpm = semver.gte(npmVersion, '5.0.0'); + hasMinNpm = semver.gte(npmVersion, usePnpm ? '3.8.1' : '5.0.0'); } catch (err) { // ignore } @@ -857,7 +875,7 @@ function getProxy() { } } } -function checkThatNpmCanReadCwd() { +function checkThatNpmCanReadCwd(pnpm) { const cwd = process.cwd(); let childOutput = null; try { @@ -866,7 +884,9 @@ function checkThatNpmCanReadCwd() { // `npm config list` is the only reliable way I could find // to reproduce the wrong path. Just printing process.cwd() // in a Node process was not enough. - childOutput = spawn.sync('npm', ['config', 'list']).output.join(''); + childOutput = spawn + .sync(pnpm ? 'pnpm' : 'npm', ['config', 'list']) + .output.join(''); } catch (err) { // Something went wrong spawning node. // Not great, but it means we can't do this check. diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md index d0399fd8c72..48df82e013c 100644 --- a/packages/react-dev-utils/README.md +++ b/packages/react-dev-utils/README.md @@ -306,11 +306,11 @@ if (openBrowser('http://localhost:3000')) { } ``` -#### `printHostingInstructions(appPackage: Object, publicUrl: string, publicPath: string, buildFolder: string, useYarn: boolean): void` +#### `printHostingInstructions(appPackage: Object, publicUrl: string, publicPath: string, buildFolder: string, useYarn: boolean, usePnpm: boolean): void` Prints hosting instructions after the project is built. -Pass your parsed `package.json` object as `appPackage`, your the URL where you plan to host the app as `publicUrl`, `output.publicPath` from your Webpack configuration as `publicPath`, the `buildFolder` name, and whether to `useYarn` in instructions. +Pass your parsed `package.json` object as `appPackage`, your the URL where you plan to host the app as `publicUrl`, `output.publicPath` from your Webpack configuration as `publicPath`, the `buildFolder` name, and whether to `useYarn` or `usePnpm` in instructions. ```js const appPackage = require(paths.appPackageJson); @@ -336,6 +336,7 @@ The `args` object accepts a number of properties: - **devSocket** `Object`: Required if `useTypeScript` is `true`. This object should include `errors` and `warnings` which are functions accepting an array of errors or warnings emitted by the type checking. This is useful when running `fork-ts-checker-webpack-plugin` with `async: true` to report errors that are emitted after the webpack build is complete. - **urls** `Object`: To provide the `urls` argument, use `prepareUrls()` described below. - **useYarn** `boolean`: If `true`, yarn instructions will be emitted in the terminal instead of npm. +- **usePnpm** `boolean`: If `true`, pnpm instructions will be emitted in the terminal instead of npm. - **useTypeScript** `boolean`: If `true`, TypeScript type checking will be enabled. Be sure to provide the `devSocket` argument above if this is set to `true`. - **tscCompileOnError** `boolean`: If `true`, errors in TypeScript type checking will not prevent start script from running app, and will not cause build script to exit unsuccessfully. Also downgrades all TypeScript type checking error messages to warning messages. - **webpack** `function`: A reference to the webpack constructor. diff --git a/packages/react-dev-utils/WebpackDevServerUtils.js b/packages/react-dev-utils/WebpackDevServerUtils.js index d43f141c387..a1cddaf50a1 100644 --- a/packages/react-dev-utils/WebpackDevServerUtils.js +++ b/packages/react-dev-utils/WebpackDevServerUtils.js @@ -76,7 +76,7 @@ function prepareUrls(protocol, host, port) { }; } -function printInstructions(appName, urls, useYarn) { +function printInstructions(appName, urls, useYarn, usePnpm) { console.log(); console.log(`You can now view ${chalk.bold(appName)} in the browser.`); console.log(); @@ -96,7 +96,9 @@ function printInstructions(appName, urls, useYarn) { console.log('Note that the development build is not optimized.'); console.log( `To create a production build, use ` + - `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.` + `${chalk.cyan( + `${useYarn ? 'yarn' : `${usePnpm ? 'pnpm' : 'npm'} run`} build` + )}.` ); console.log(); } @@ -107,6 +109,7 @@ function createCompiler({ devSocket, urls, useYarn, + usePnpm, useTypeScript, tscCompileOnError, webpack, @@ -228,7 +231,7 @@ function createCompiler({ console.log(chalk.green('Compiled successfully!')); } if (isSuccessful && (isInteractive || isFirstCompile)) { - printInstructions(appName, urls, useYarn); + printInstructions(appName, urls, useYarn, usePnpm); } isFirstCompile = false; diff --git a/packages/react-dev-utils/printHostingInstructions.js b/packages/react-dev-utils/printHostingInstructions.js index 39d7de0fe7f..7bb9d8bc9f0 100644 --- a/packages/react-dev-utils/printHostingInstructions.js +++ b/packages/react-dev-utils/printHostingInstructions.js @@ -17,7 +17,8 @@ function printHostingInstructions( publicUrl, publicPath, buildFolder, - useYarn + useYarn, + usePnpm ) { if (publicUrl && publicUrl.includes('.github.io/')) { // "homepage": "http://user.github.io/project" @@ -25,7 +26,7 @@ function printHostingInstructions( const hasDeployScript = typeof appPackage.scripts.deploy !== 'undefined'; printBaseMessage(buildFolder, publicPathname); - printDeployInstructions(publicUrl, hasDeployScript, useYarn); + printDeployInstructions(publicUrl, hasDeployScript, useYarn, usePnpm); } else if (publicPath !== '/') { // "homepage": "http://mywebsite.com/project" printBaseMessage(buildFolder, publicPath); @@ -34,7 +35,7 @@ function printHostingInstructions( // or no homepage printBaseMessage(buildFolder, publicUrl); - printStaticServerInstructions(buildFolder, useYarn); + printStaticServerInstructions(buildFolder, useYarn, usePnpm); } console.log(); console.log('Find out more about deployment here:'); @@ -69,7 +70,7 @@ function printBaseMessage(buildFolder, hostingLocation) { console.log(`The ${chalk.cyan(buildFolder)} folder is ready to be deployed.`); } -function printDeployInstructions(publicUrl, hasDeployScript, useYarn) { +function printDeployInstructions(publicUrl, hasDeployScript, useYarn, usePnpm) { console.log(`To publish it at ${chalk.green(publicUrl)} , run:`); console.log(); @@ -78,7 +79,9 @@ function printDeployInstructions(publicUrl, hasDeployScript, useYarn) { if (useYarn) { console.log(` ${chalk.cyan('yarn')} add --dev gh-pages`); } else { - console.log(` ${chalk.cyan('npm')} install --save-dev gh-pages`); + console.log( + ` ${chalk.cyan(usePnpm ? 'pnpm' : 'npm')} install --save-dev gh-pages` + ); } console.log(); @@ -92,7 +95,7 @@ function printDeployInstructions(publicUrl, hasDeployScript, useYarn) { console.log(` ${chalk.dim('// ...')}`); console.log( ` ${chalk.yellow('"predeploy"')}: ${chalk.yellow( - `"${useYarn ? 'yarn' : 'npm run'} build",` + `"${useYarn ? 'yarn' : `${usePnpm ? 'pnpm' : 'npm'} run`} build",` )}` ); console.log( @@ -106,10 +109,14 @@ function printDeployInstructions(publicUrl, hasDeployScript, useYarn) { console.log('Then run:'); console.log(); } - console.log(` ${chalk.cyan(useYarn ? 'yarn' : 'npm')} run deploy`); + console.log( + ` ${chalk.cyan( + useYarn ? 'yarn' : `${usePnpm ? 'pnpm' : 'npm'}` + )} run deploy` + ); } -function printStaticServerInstructions(buildFolder, useYarn) { +function printStaticServerInstructions(buildFolder, useYarn, usePnpm) { console.log('You may serve it with a static server:'); console.log(); @@ -117,7 +124,9 @@ function printStaticServerInstructions(buildFolder, useYarn) { if (useYarn) { console.log(` ${chalk.cyan('yarn')} global add serve`); } else { - console.log(` ${chalk.cyan('npm')} install -g serve`); + console.log( + ` ${chalk.cyan(`${usePnpm ? 'pnpm' : 'npm'}`)} install -g serve` + ); } } console.log(` ${chalk.cyan('serve')} -s ${buildFolder}`); diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index e5a3e0b5374..8c5794d5e5a 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -86,6 +86,7 @@ module.exports = { appTsConfig: resolveApp('tsconfig.json'), appJsConfig: resolveApp('jsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), + pnpmLockFile: resolveApp('pnpm-lock.yaml'), testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index 5bbc8741136..3841d0cca2c 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -47,6 +47,7 @@ const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; const useYarn = fs.existsSync(paths.yarnLockFile); +const usePnpm = fs.existsSync(paths.pnpmLockFile); // These sizes are pretty large. We'll warn for bundles exceeding them. const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; @@ -118,7 +119,8 @@ checkBrowsers(paths.appPath, isInteractive) publicUrl, publicPath, buildFolder, - useYarn + useYarn, + usePnpm ); }, err => { diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js index b0460036635..698c4a65d79 100644 --- a/packages/react-scripts/scripts/init.js +++ b/packages/react-scripts/scripts/init.js @@ -87,6 +87,7 @@ module.exports = function( ); const appPackage = require(path.join(appPath, 'package.json')); const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); + const usePnpm = fs.existsSync(path.join(appPath, 'pnpm-lock.yaml')); // Copy over some of the devDependencies appPackage.dependencies = appPackage.dependencies || {}; @@ -179,7 +180,7 @@ module.exports = function( command = 'yarnpkg'; args = ['add']; } else { - command = 'npm'; + command = usePnpm ? 'pnpm' : 'npm'; args = ['install', '--save', verbose && '--verbose'].filter(e => e); } args.push('react', 'react-dom'); @@ -233,7 +234,7 @@ module.exports = function( } // Change displayed command to yarn instead of yarnpkg - const displayedCommand = useYarn ? 'yarn' : 'npm'; + const displayedCommand = useYarn ? 'yarn' : usePnpm ? 'pnpm' : 'npm'; console.log(); console.log(`Success! Created ${appName} at ${appPath}`); diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 6c2602f04ff..7b9b890bb6d 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -49,6 +49,7 @@ const configFactory = require('../config/webpack.config'); const createDevServerConfig = require('../config/webpackDevServer.config'); const useYarn = fs.existsSync(paths.yarnLockFile); +const usePnpm = fs.existsSync(paths.pnpmLockFile); const isInteractive = process.stdout.isTTY; // Warn and crash if required files are missing @@ -110,6 +111,7 @@ checkBrowsers(paths.appPath, isInteractive) devSocket, urls, useYarn, + usePnpm, useTypeScript, tscCompileOnError, webpack, diff --git a/tasks/e2e-installs.sh b/tasks/e2e-installs.sh index f083f2dd09b..d161e40f42e 100755 --- a/tasks/e2e-installs.sh +++ b/tasks/e2e-installs.sh @@ -146,6 +146,20 @@ exists node_modules/react-scripts grep '"version": "1.0.17"' node_modules/react-scripts/package.json checkDependencies +# ****************************************************************************** +# Test --use-pnpm flag +# ****************************************************************************** + +cd "$temp_app_path" +npx create-react-app test-use-pnpm-flag --use-pnpm --scripts-version=1.0.17 +cd test-use-pnpm-flag + +# Check corresponding scripts version is installed. +exists node_modules/react-scripts +[ ! -e "yarn.lock" ] && echo "yarn.lock correctly does not exist" +grep '"version": "1.0.17"' node_modules/react-scripts/package.json +checkDependencies + # ****************************************************************************** # Test --typescript flag # ******************************************************************************