diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 40ba056c10a..ff748b46339 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -413,6 +413,10 @@ jobs: path: ./* key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }} + - uses: pnpm/action-setup@v2.2.1 + with: + version: 6.32.2 + - uses: actions/download-artifact@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: diff --git a/docs/api-reference/create-next-app.md b/docs/api-reference/create-next-app.md index f463840b88b..e291dd5e94f 100644 --- a/docs/api-reference/create-next-app.md +++ b/docs/api-reference/create-next-app.md @@ -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? diff --git a/packages/create-next-app/README.md b/packages/create-next-app/README.md index 16b1687fc88..12ebd9d5cd0 100644 --- a/packages/create-next-app/README.md +++ b/packages/create-next-app/README.md @@ -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` ## Why use Create Next App? diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index c8cc31f36f0..56b2c46277c 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -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 @@ -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() @@ -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. */ @@ -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. */ @@ -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() } diff --git a/packages/create-next-app/helpers/get-pkg-manager.ts b/packages/create-next-app/helpers/get-pkg-manager.ts new file mode 100644 index 00000000000..e3de28a4f9d --- /dev/null +++ b/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' + } +} diff --git a/packages/create-next-app/helpers/install.ts b/packages/create-next-app/helpers/install.ts index 09337e17a6d..8a36345297a 100644 --- a/packages/create-next-app/helpers/install.ts +++ b/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. */ @@ -25,10 +26,10 @@ interface InstallArgs { export function install( root: string, dependencies: string[] | null, - { useYarn, isOnline, devDependencies }: InstallArgs + { packageManager, isOnline, devDependencies }: InstallArgs ): Promise { /** - * NPM-specific command-line flags. + * (p)npm-specific command-line flags. */ const npmFlags: string[] = [] /** @@ -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) { /** @@ -57,7 +59,7 @@ 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') @@ -65,7 +67,7 @@ export function install( } } else { /** - * If there are no dependencies, run a variation of `{displayCommand} + * If there are no dependencies, run a variation of `{packageManager} * install`. */ args = ['install'] diff --git a/packages/create-next-app/helpers/should-use-yarn.ts b/packages/create-next-app/helpers/should-use-yarn.ts deleted file mode 100644 index c305361517f..00000000000 --- a/packages/create-next-app/helpers/should-use-yarn.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { execSync } from 'child_process' - -export function shouldUseYarn(): boolean { - try { - const userAgent = process.env.npm_config_user_agent - if (userAgent) { - return Boolean(userAgent && userAgent.startsWith('yarn')) - } - execSync('yarnpkg --version', { stdio: 'ignore' }) - return true - } catch (e) { - return false - } -} diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index a9475217114..8d7f2542761 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -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' @@ -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( @@ -116,14 +123,19 @@ async function run(): Promise { 'Please provide an example name or url, otherwise remove the example option.' ) process.exit(1) - return } + const packageManager = !!program.useNpm + ? 'npm' + : !!program.usePnpm + ? 'pnpm' + : 'yarn' + 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, @@ -147,7 +159,7 @@ async function run(): Promise { await createApp({ appPath: resolvedProjectPath, - useNpm: !!program.useNpm, + packageManager, typescript: program.typescript, }) } @@ -159,7 +171,7 @@ async function notifyUpdate(): Promise { try { const res = await update if (res?.latest) { - const isYarn = shouldUseYarn() + const pkgManager = getPkgManager() console.log() console.log( @@ -168,9 +180,9 @@ async function notifyUpdate(): Promise { 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` ) ) console.log() diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.ts similarity index 85% rename from test/integration/create-next-app/index.test.js rename to test/integration/create-next-app/index.test.ts index dccbfc22251..1f68f56875c 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.ts @@ -9,9 +9,10 @@ const cli = require.resolve('create-next-app/dist/index.js') const exampleRepo = 'https://github.com/vercel/next.js/tree/canary' const examplePath = 'examples/basic-css' -const run = (args, options) => execa('node', [cli].concat(args), options) +const run = (args: string[], options: execa.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 { @@ -104,34 +105,24 @@ describe('create next app', () => { const res = await run([projectName, '--typescript'], { cwd }) expect(res.exitCode).toBe(0) - const pkgJSONPath = path.join(cwd, projectName, 'package.json') + const files = [ + 'package.json', + 'pages/index.tsx', + 'pages/_app.tsx', + 'pages/api/hello.ts', + 'tsconfig.json', + 'next-env.d.ts', + '.eslintrc.json', + 'node_modules/next', + // check we copied default `.gitignore` + '.gitignore', + ] - expect(fs.existsSync(pkgJSONPath)).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'pages/index.tsx')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'pages/_app.tsx')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'pages/api/hello.ts')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'tsconfig.json')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'next-env.d.ts')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, '.eslintrc.json')) - ).toBeTruthy() - expect( - fs.existsSync(path.join(cwd, projectName, 'node_modules/next')) - ).toBe(true) - // check we copied default `.gitignore` - expect( - fs.existsSync(path.join(cwd, projectName, '.gitignore')) - ).toBeTruthy() + files.forEach((file) => + expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy() + ) + + const pkgJSONPath = path.join(cwd, projectName, 'package.json') // Assert for dependencies specific to the typescript template const pkgJSON = require(pkgJSONPath) @@ -395,4 +386,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() + ) + }) + }) })