Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pnpm package manager #7791

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docusaurus/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 32 additions & 12 deletions packages/create-react-app/createReactApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -179,6 +180,7 @@ createApp(
program.verbose,
program.scriptsVersion,
program.useNpm,
program.usePnpm,
program.usePnp,
program.typescript,
hiddenProgram.internalTestingTemplate
Expand All @@ -189,6 +191,7 @@ function createApp(
verbose,
version,
useNpm,
usePnpm,
usePnp,
useTypescript,
template
Expand All @@ -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);
}

Expand All @@ -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`
)
);
Expand Down Expand Up @@ -289,6 +294,7 @@ function createApp(
originalDirectory,
template,
useYarn,
usePnpm,
usePnp,
useTypescript
);
Expand All @@ -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;
Expand Down Expand Up @@ -332,7 +346,7 @@ function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
console.log();
}
} else {
command = 'npm';
command = usePnpm ? 'pnpm' : 'npm';
args = [
'install',
'--save',
Expand All @@ -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();
}
Expand Down Expand Up @@ -373,6 +389,7 @@ function run(
originalDirectory,
template,
useYarn,
usePnpm,
usePnp,
useTypescript
) {
Expand Down Expand Up @@ -411,6 +428,7 @@ function run(
return install(
root,
useYarn,
usePnpm,
usePnp,
allDependencies,
verbose,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -857,7 +875,7 @@ function getProxy() {
}
}
}
function checkThatNpmCanReadCwd() {
function checkThatNpmCanReadCwd(pnpm) {
const cwd = process.cwd();
let childOutput = null;
try {
Expand All @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions packages/react-dev-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions packages/react-dev-utils/WebpackDevServerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
Expand All @@ -107,6 +109,7 @@ function createCompiler({
devSocket,
urls,
useYarn,
usePnpm,
useTypeScript,
tscCompileOnError,
webpack,
Expand Down Expand Up @@ -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;

Expand Down
27 changes: 18 additions & 9 deletions packages/react-dev-utils/printHostingInstructions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ function printHostingInstructions(
publicUrl,
publicPath,
buildFolder,
useYarn
useYarn,
usePnpm
) {
if (publicUrl && publicUrl.includes('.github.io/')) {
// "homepage": "http://user.github.io/project"
const publicPathname = url.parse(publicPath).pathname;
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);
Expand All @@ -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:');
Expand Down Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -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(
Expand All @@ -106,18 +109,24 @@ 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();

if (!fs.existsSync(`${globalModules}/serve`)) {
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}`);
Expand Down
1 change: 1 addition & 0 deletions packages/react-scripts/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
4 changes: 3 additions & 1 deletion packages/react-scripts/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,7 +119,8 @@ checkBrowsers(paths.appPath, isInteractive)
publicUrl,
publicPath,
buildFolder,
useYarn
useYarn,
usePnpm
);
},
err => {
Expand Down
5 changes: 3 additions & 2 deletions packages/react-scripts/scripts/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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}`);
Expand Down
2 changes: 2 additions & 0 deletions packages/react-scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,6 +111,7 @@ checkBrowsers(paths.appPath, isInteractive)
devSocket,
urls,
useYarn,
usePnpm,
useTypeScript,
tscCompileOnError,
webpack,
Expand Down
14 changes: 14 additions & 0 deletions tasks/e2e-installs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ******************************************************************************
Expand Down