rsnext/packages/create-next-app/create-app.ts

324 lines
8.9 KiB
TypeScript
Raw Normal View History

/* eslint-disable import/no-extraneous-dependencies */
import retry from 'async-retry'
import chalk from 'chalk'
import cpy from 'cpy'
import fs from 'fs'
import os from 'os'
import path from 'path'
import {
downloadAndExtractExample,
downloadAndExtractRepo,
getRepoInfo,
hasExample,
hasRepo,
RepoInfo,
} from './helpers/examples'
import { makeDir } from './helpers/make-dir'
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'
export class DownloadError extends Error {}
export async function createApp({
appPath,
useNpm,
example,
examplePath,
typescript,
}: {
appPath: string
useNpm: boolean
example?: string
examplePath?: string
typescript?: boolean
}): Promise<void> {
let repoInfo: RepoInfo | undefined
const template = typescript ? 'typescript' : 'default'
if (example) {
let repoUrl: URL | undefined
try {
repoUrl = new URL(example)
} catch (error: any) {
if (error.code !== 'ERR_INVALID_URL') {
console.error(error)
process.exit(1)
}
}
if (repoUrl) {
if (repoUrl.origin !== 'https://github.com') {
console.error(
`Invalid URL: ${chalk.red(
`"${example}"`
)}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
)
process.exit(1)
}
repoInfo = await getRepoInfo(repoUrl, examplePath)
if (!repoInfo) {
console.error(
`Found invalid GitHub URL: ${chalk.red(
`"${example}"`
)}. Please fix the URL and try again.`
)
process.exit(1)
}
const found = await hasRepo(repoInfo)
if (!found) {
console.error(
`Could not locate the repository for ${chalk.red(
`"${example}"`
)}. Please check that the repository exists and try again.`
)
process.exit(1)
}
} else if (example !== '__internal-testing-retry') {
const found = await hasExample(example)
if (!found) {
console.error(
`Could not locate an example named ${chalk.red(
`"${example}"`
)}. It could be due to the following:\n`,
`1. Your spelling of example ${chalk.red(
`"${example}"`
)} might be incorrect.\n`,
`2. You might not be connected to the internet.`
)
process.exit(1)
}
}
}
const root = path.resolve(appPath)
if (!(await isWriteable(path.dirname(root)))) {
console.error(
'The application path is not writable, please check folder permissions and try again.'
)
console.error(
'It is likely you do not have write permissions for this folder.'
)
process.exit(1)
}
const appName = path.basename(root)
await makeDir(root)
if (!isFolderEmpty(root, appName)) {
process.exit(1)
}
const useYarn = useNpm ? false : shouldUseYarn()
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()
await makeDir(root)
process.chdir(root)
if (example) {
/**
* If an example repository is provided, clone it.
*/
try {
if (repoInfo) {
const repoInfo2 = repoInfo
console.log(
`Downloading files from repo ${chalk.cyan(
example
)}. This might take a moment.`
)
console.log()
await retry(() => downloadAndExtractRepo(root, repoInfo2), {
retries: 3,
})
} else {
console.log(
`Downloading files for example ${chalk.cyan(
example
)}. This might take a moment.`
)
console.log()
await retry(() => downloadAndExtractExample(root, example), {
retries: 3,
})
}
} catch (reason) {
function isErrorLike(err: unknown): err is { message: string } {
return (
typeof err === 'object' &&
err !== null &&
typeof (err as { message?: unknown }).message === 'string'
)
}
throw new DownloadError(
isErrorLike(reason) ? reason.message : reason + ''
)
}
// Copy our default `.gitignore` if the application did not provide one
const ignorePath = path.join(root, '.gitignore')
if (!fs.existsSync(ignorePath)) {
fs.copyFileSync(
path.join(__dirname, 'templates', template, 'gitignore'),
ignorePath
)
}
next-env.d.ts note in templates (#27983) Hello! I was using `npx create-next-app --ts` to quickly bootstrap a basic next.js project with Typescript. It bothered me that I got git diff just from running `npm run build` inside the project, because the new notice in the `next-env.d.ts` file. ![image](https://user-images.githubusercontent.com/13413409/129115266-80e00bf3-78aa-40be-bd0f-a18aaa448a68.png) So I went ahead and updated the `next-env.d.ts` file in [`packages/create-next-app/templates/typescript`](https://github.com/oBusk/next.js/blob/fb67ce9864f3561bd8883092d215960040402aa7/packages/create-next-app/templates/typescript/next-env.d.ts) to be exactly how the file looks after running once. (https://github.com/vercel/next.js/commit/7417ecc09c6b44fb4ebfdf6acd13cbcec784a44e) Then I realized that I could probably do the same for all the `next-env.d.ts` that are spread out in the many examples, to make running those examples after cloning them feel a bit smoother. (https://github.com/vercel/next.js/commit/fb67ce9864f3561bd8883092d215960040402aa7) > However I skipped the [`with-typescript-graphql`](https://github.com/vercel/next.js/tree/40f85f6d95b3f69421bbf13cced68754e009eed4/examples/with-typescript-graphql) example since that [`next-env.d.ts`](https://github.com/vercel/next.js/blob/40f85f6d95b3f69421bbf13cced68754e009eed4/examples/with-typescript-graphql/next-env.d.ts#L4-L9) actually has changes and I couldn't even run `yarn build` in that example, so I'm not sure what will happen with the `next-env.d.ts`. Someone who _gets_ that example will have to fix it I suppose.
2021-08-12 22:36:53 +02:00
// Copy default `next-env.d.ts` to any example that is typescript
const tsconfigPath = path.join(root, 'tsconfig.json')
if (fs.existsSync(tsconfigPath)) {
fs.copyFileSync(
path.join(__dirname, 'templates', 'typescript', 'next-env.d.ts'),
path.join(root, 'next-env.d.ts')
)
}
console.log('Installing packages. This might take a couple of minutes.')
console.log()
await install(root, null, { useYarn, 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}.`))
/**
* Create a package.json for the new project.
*/
const packageJson = {
name: appName,
private: true,
scripts: {
dev: 'next dev',
build: 'next build',
start: 'next start',
lint: 'next lint',
},
}
/**
* Write it to disk.
*/
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
)
/**
* These flags will be passed to `install()`.
*/
const installFlags = { useYarn, isOnline }
/**
* Default dependencies.
*/
const dependencies = ['react', 'react-dom', 'next']
/**
* Default devDependencies.
*/
const devDependencies = ['eslint', 'eslint-config-next']
/**
* TypeScript projects will have type definitions and other devDependencies.
*/
if (typescript) {
devDependencies.push('typescript', '@types/react', '@types/node')
}
/**
* Install package.json dependencies if they exist.
*/
if (dependencies.length) {
console.log()
console.log('Installing dependencies:')
for (const dependency of dependencies) {
console.log(`- ${chalk.cyan(dependency)}`)
}
console.log()
await install(root, dependencies, installFlags)
}
/**
* Install package.json devDependencies if they exist.
*/
if (devDependencies.length) {
console.log()
console.log('Installing devDependencies:')
for (const devDependency of devDependencies) {
console.log(`- ${chalk.cyan(devDependency)}`)
}
console.log()
const devInstallFlags = { devDependencies: true, ...installFlags }
await install(root, devDependencies, devInstallFlags)
}
console.log()
/**
* Copy the template files to the target directory.
*/
await cpy('**', root, {
parents: true,
cwd: path.join(__dirname, 'templates', template),
2020-05-18 21:24:37 +02:00
rename: (name) => {
switch (name) {
case 'gitignore':
[ESLint] Introduce a new setup process when `next lint` is run for the first time (#26584) This PR introduces an improved developer experience when `next lint` is run for the first time. ### Current behavior `eslint-config-next` is a required package that must be installed before proceeding with `next lint` or `next build`: ![image](https://user-images.githubusercontent.com/12476932/123468791-43088100-d5c0-11eb-9ad0-5beb80b6c968.png) Although this has helped many developers start using the new ESLint config, this has also resulted in a few issues: - Users are required to install the full config (`eslint-config-next`) even if they do not use it or use the Next.js plugin directly (`eslint-plugin-next`). - #26348 - There's some confusion on why `eslint-config-next` needs to be installed or how it should be used instead of `eslint-plugin-next`. - #26574 - #26475 - #26438 ### New behavior Instead of enforcing `eslint-config-next` as a required package, this PR prompts the user by asking what config they would like to start. This happens when `next lint` is run for the first time **and** if no ESLint configuration is detected in the application. <img src="https://user-images.githubusercontent.com/12476932/124331177-e1668a80-db5c-11eb-8915-38d3dc20f5d4.gif" width="800" /> - The CLI will take care of installing `eslint` or `eslint-config-next` if either is not already installed - Users now have the option to choose between a strict configuration (`next/core-web-vitals`) or just the base configuration (`next`) - For users that decide to create their own ESLint configuration, or already have an existing one, **installing `eslint-config-next` will not be a requirement for `next lint` or `next build` to run**. A warning message will just show if the Next.js ESLint plugin is not detected in an ESLint config. <img width="682" alt="Screen Shot 2021-06-25 at 3 02 12 PM" src="https://user-images.githubusercontent.com/12476932/123473329-6cc4a680-d5c6-11eb-9a57-d5c0b89a2732.png"> --- In addition, this PR also: - Fixes #26348 - Updates documentation to make it more clear what approach to take for new and existing ESLint configurations
2021-08-04 23:53:15 +02:00
case 'eslintrc.json': {
return '.'.concat(name)
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case 'README-template.md': {
return 'README.md'
}
default: {
return name
}
}
},
})
}
if (tryGitInit(root)) {
console.log('Initialized a git repository.')
console.log()
}
let cdpath: string
if (path.join(originalDirectory, appName) === appPath) {
cdpath = appName
} else {
cdpath = appPath
}
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(' Starts the development server.')
console.log()
console.log(chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`))
console.log(' Builds the app for production.')
console.log()
console.log(chalk.cyan(` ${displayedCommand} 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`)}`
)
console.log()
}