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

feat: support pnpm with create-next-app #34947

Merged
merged 12 commits into from Mar 3, 2022
1 change: 1 addition & 0 deletions docs/api-reference/create-next-app.md
Expand Up @@ -28,6 +28,7 @@ yarn create next-app --typescript
- **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/canary/examples) or a GitHub URL. The URL can use any branch and/or subdirectory.
- **--example-path [path-to-example]** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar`
- **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm. To bootstrap using yarn we recommend running `yarn create next-app`
- **--use-pnpm** - Explicitly tell the CLI to bootstrap the app using pnpm. To bootstrap using yarn we recommend running `yarn create next-app`

### Why use Create Next App?

Expand Down
1 change: 1 addition & 0 deletions packages/create-next-app/README.md
Expand Up @@ -26,6 +26,7 @@ npx create-next-app blog-app
- **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/canary/examples) or a GitHub URL. The URL can use any branch and/or subdirectory.
- **--example-path <path-to-example>** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar`
- **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm. To bootstrap using yarn we recommend to run `yarn create next-app`
- **--use-pnpm** - Explicitly tell the CLI to bootstrap the app using pnpm. To bootstrap using yarn we recommend running `yarn create next-app`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"To bootstrap using yarn"...

is that accurate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our default is yarn right now. 👍 So if no --use-npm or --use-pnpm flags are defined, we are falling back to Yarn.


## Why use Create Next App?

Expand Down
23 changes: 11 additions & 12 deletions packages/create-next-app/create-app.ts
Expand Up @@ -18,20 +18,20 @@ import { tryGitInit } from './helpers/git'
import { install } from './helpers/install'
import { isFolderEmpty } from './helpers/is-folder-empty'
import { getOnline } from './helpers/is-online'
import { shouldUseYarn } from './helpers/should-use-yarn'
import { isWriteable } from './helpers/is-writeable'
import type { PackageManager } from './helpers/get-pkg-manager'

export class DownloadError extends Error {}

export async function createApp({
appPath,
useNpm,
packageManager,
example,
examplePath,
typescript,
}: {
appPath: string
useNpm: boolean
packageManager: PackageManager
example?: string
examplePath?: string
typescript?: boolean
Expand Down Expand Up @@ -119,11 +119,10 @@ export async function createApp({
process.exit(1)
}

const useYarn = useNpm ? false : shouldUseYarn()
const useYarn = packageManager === 'yarn'
const isOnline = !useYarn || (await getOnline())
const originalDirectory = process.cwd()

const displayedCommand = useYarn ? 'yarn' : 'npm'
console.log(`Creating a new Next.js app in ${chalk.green(root)}.`)
console.log()

Expand Down Expand Up @@ -189,14 +188,14 @@ export async function createApp({
console.log('Installing packages. This might take a couple of minutes.')
console.log()

await install(root, null, { useYarn, isOnline })
await install(root, null, { packageManager, isOnline })
console.log()
} else {
/**
* Otherwise, if an example repository is not provided for cloning, proceed
* by installing from a template.
*/
console.log(chalk.bold(`Using ${displayedCommand}.`))
console.log(chalk.bold(`Using ${packageManager}.`))
/**
* Create a package.json for the new project.
*/
Expand All @@ -221,7 +220,7 @@ export async function createApp({
/**
* These flags will be passed to `install()`.
*/
const installFlags = { useYarn, isOnline }
const installFlags = { packageManager, isOnline }
/**
* Default dependencies.
*/
Expand Down Expand Up @@ -309,20 +308,20 @@ export async function createApp({
console.log(`${chalk.green('Success!')} Created ${appName} at ${appPath}`)
console.log('Inside that directory, you can run several commands:')
console.log()
console.log(chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}dev`))
console.log(chalk.cyan(` ${packageManager} ${useYarn ? '' : 'run '}dev`))
console.log(' Starts the development server.')
console.log()
console.log(chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`))
console.log(chalk.cyan(` ${packageManager} ${useYarn ? '' : 'run '}build`))
console.log(' Builds the app for production.')
console.log()
console.log(chalk.cyan(` ${displayedCommand} start`))
console.log(chalk.cyan(` ${packageManager} start`))
console.log(' Runs the built app in production mode.')
console.log()
console.log('We suggest that you begin by typing:')
console.log()
console.log(chalk.cyan(' cd'), cdpath)
console.log(
` ${chalk.cyan(`${displayedCommand} ${useYarn ? '' : 'run '}dev`)}`
` ${chalk.cyan(`${packageManager} ${useYarn ? '' : 'run '}dev`)}`
)
console.log()
}
25 changes: 25 additions & 0 deletions packages/create-next-app/helpers/get-pkg-manager.ts
@@ -0,0 +1,25 @@
import { execSync } from 'child_process'

export type PackageManager = 'npm' | 'pnpm' | 'yarn'

export function getPkgManager(): PackageManager {
try {
const userAgent = process.env.npm_config_user_agent
if (userAgent) {
if (userAgent.startsWith('yarn')) {
return 'yarn'
} else if (userAgent.startsWith('pnpm')) {
return 'pnpm'
}
}
try {
execSync('yarn --version', { stdio: 'ignore' })
return 'yarn'
} catch {
execSync('pnpm --version', { stdio: 'ignore' })
return 'pnpm'
}
} catch {
return 'npm'
}
}
18 changes: 10 additions & 8 deletions packages/create-next-app/helpers/install.ts
@@ -1,12 +1,13 @@
/* eslint-disable import/no-extraneous-dependencies */
import chalk from 'chalk'
import spawn from 'cross-spawn'
import type { PackageManager } from './get-pkg-manager'

interface InstallArgs {
/**
* Indicate whether to install packages using Yarn.
* Indicate whether to install packages using npm, pnpm or Yarn.
*/
useYarn: boolean
packageManager: PackageManager
/**
* Indicate whether there is an active Internet connection.
*/
Expand All @@ -25,10 +26,10 @@ interface InstallArgs {
export function install(
root: string,
dependencies: string[] | null,
{ useYarn, isOnline, devDependencies }: InstallArgs
{ packageManager, isOnline, devDependencies }: InstallArgs
): Promise<void> {
/**
* NPM-specific command-line flags.
* (p)npm-specific command-line flags.
*/
const npmFlags: string[] = []
/**
Expand All @@ -40,11 +41,12 @@ export function install(
*/
return new Promise((resolve, reject) => {
let args: string[]
let command: string = useYarn ? 'yarnpkg' : 'npm'
let command = packageManager
const useYarn = packageManager === 'yarn'

if (dependencies && dependencies.length) {
/**
* If there are dependencies, run a variation of `{displayCommand} add`.
* If there are dependencies, run a variation of `{packageManager} add`.
*/
if (useYarn) {
/**
Expand All @@ -57,15 +59,15 @@ export function install(
args.push(...dependencies)
} else {
/**
* Call `npm install [--save|--save-dev] ...`.
* Call `(p)npm install [--save|--save-dev] ...`.
*/
args = ['install', '--save-exact']
args.push(devDependencies ? '--save-dev' : '--save')
args.push(...dependencies)
}
} else {
/**
* If there are no dependencies, run a variation of `{displayCommand}
* If there are no dependencies, run a variation of `{packageManager}
* install`.
*/
args = ['install']
Expand Down
14 changes: 0 additions & 14 deletions packages/create-next-app/helpers/should-use-yarn.ts

This file was deleted.

26 changes: 19 additions & 7 deletions packages/create-next-app/index.ts
Expand Up @@ -6,7 +6,7 @@ import path from 'path'
import prompts from 'prompts'
import checkForUpdate from 'update-check'
import { createApp, DownloadError } from './create-app'
import { shouldUseYarn } from './helpers/should-use-yarn'
import { getPkgManager } from './helpers/get-pkg-manager'
import { validateNpmName } from './helpers/validate-pkg'
import packageJson from './package.json'

Expand All @@ -31,6 +31,13 @@ const program = new Commander.Command(packageJson.name)
`

Explicitly tell the CLI to bootstrap the app using npm
`
)
.option(
'--use-pnpm',
`

Explicitly tell the CLI to bootstrap the app using pnpm
`
)
.option(
Expand Down Expand Up @@ -116,14 +123,19 @@ async function run(): Promise<void> {
'Please provide an example name or url, otherwise remove the example option.'
)
process.exit(1)
return
}

const packageManager = !!program.useYarn
? 'yarn'
: !!program.usePnpm
? 'pnpm'
: 'npm'

const example = typeof program.example === 'string' && program.example.trim()
try {
await createApp({
appPath: resolvedProjectPath,
useNpm: !!program.useNpm,
packageManager,
example: example && example !== 'default' ? example : undefined,
examplePath: program.examplePath,
typescript: program.typescript,
Expand All @@ -147,7 +159,7 @@ async function run(): Promise<void> {

await createApp({
appPath: resolvedProjectPath,
useNpm: !!program.useNpm,
packageManager,
typescript: program.typescript,
})
}
Expand All @@ -159,7 +171,7 @@ async function notifyUpdate(): Promise<void> {
try {
const res = await update
if (res?.latest) {
const isYarn = shouldUseYarn()
const pkgManager = getPkgManager()

console.log()
console.log(
Expand All @@ -168,9 +180,9 @@ async function notifyUpdate(): Promise<void> {
console.log(
'You can update by running: ' +
chalk.cyan(
isYarn
pkgManager === 'yarn'
? 'yarn global add create-next-app'
: 'npm i -g create-next-app'
: `${pkgManager} install --global create-next-app`
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved
)
)
console.log()
Expand Down
Expand Up @@ -11,7 +11,7 @@ const examplePath = 'examples/basic-css'

const run = (args, options) => execa('node', [cli].concat(args), options)

async function usingTempDir(fn, options) {
async function usingTempDir(fn: (...args: any[]) => any, options?: any) {
const folder = path.join(os.tmpdir(), Math.random().toString(36).substring(2))
await fs.mkdirp(folder, options)
try {
Expand Down Expand Up @@ -395,4 +395,52 @@ describe('create next app', () => {
)
})
})

it('should use pnpm as the package manager on supplying --use-pnpm', async () => {
await usingTempDir(async (cwd) => {
const projectName = 'use-pnpm'
const res = await run([projectName, '--use-pnpm'], { cwd })
expect(res.exitCode).toBe(0)

const files = [
'package.json',
'pages/index.js',
'.gitignore',
'.eslintrc.json',
'pnpm-lock.yaml',
'node_modules/next',
]
files.forEach((file) =>
expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy()
)
})
})

it('should use pnpm as the package manager on supplying --use-pnpm with example', async () => {
await usingTempDir(async (cwd) => {
const projectName = 'use-pnpm'
const res = await run(
[
projectName,
'--use-pnpm',
'--example',
`${exampleRepo}/${examplePath}`,
],
{ cwd }
)
expect(res.exitCode).toBe(0)

const files = [
'package.json',
'pages/index.js',
'.gitignore',
'pnpm-lock.yaml',
'node_modules/next',
]

files.forEach((file) =>
expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy()
)
})
})
})