2020-05-28 10:23:10 +02:00
|
|
|
/* eslint-disable import/no-extraneous-dependencies */
|
2020-05-26 18:39:18 +02:00
|
|
|
import retry from 'async-retry'
|
2019-07-19 21:55:30 +02:00
|
|
|
import chalk from 'chalk'
|
|
|
|
import cpy from 'cpy'
|
|
|
|
import fs from 'fs'
|
|
|
|
import os from 'os'
|
|
|
|
import path from 'path'
|
2020-02-27 16:32:33 +01:00
|
|
|
import {
|
|
|
|
downloadAndExtractExample,
|
|
|
|
downloadAndExtractRepo,
|
2020-05-26 18:39:18 +02:00
|
|
|
getRepoInfo,
|
|
|
|
hasExample,
|
|
|
|
hasRepo,
|
2020-02-27 16:32:33 +01:00
|
|
|
RepoInfo,
|
|
|
|
} from './helpers/examples'
|
2020-07-10 03:29:03 +02:00
|
|
|
import { makeDir } from './helpers/make-dir'
|
2019-11-11 06:42:51 +01:00
|
|
|
import { tryGitInit } from './helpers/git'
|
2019-07-19 21:55:30 +02:00
|
|
|
import { install } from './helpers/install'
|
|
|
|
import { isFolderEmpty } from './helpers/is-folder-empty'
|
|
|
|
import { getOnline } from './helpers/is-online'
|
2020-08-19 19:09:34 +02:00
|
|
|
import { isWriteable } from './helpers/is-writeable'
|
2022-03-04 00:49:24 +01:00
|
|
|
import type { PackageManager } from './helpers/get-pkg-manager'
|
2019-07-19 21:55:30 +02:00
|
|
|
|
2020-05-26 18:39:18 +02:00
|
|
|
export class DownloadError extends Error {}
|
|
|
|
|
2019-07-19 21:55:30 +02:00
|
|
|
export async function createApp({
|
|
|
|
appPath,
|
2022-03-04 00:49:24 +01:00
|
|
|
packageManager,
|
2019-07-19 21:55:30 +02:00
|
|
|
example,
|
2020-02-27 16:32:33 +01:00
|
|
|
examplePath,
|
2021-05-07 10:08:16 +02:00
|
|
|
typescript,
|
2019-07-19 21:55:30 +02:00
|
|
|
}: {
|
|
|
|
appPath: string
|
2022-03-04 00:49:24 +01:00
|
|
|
packageManager: PackageManager
|
2019-07-19 21:55:30 +02:00
|
|
|
example?: string
|
2020-02-27 16:32:33 +01:00
|
|
|
examplePath?: string
|
2021-05-07 10:08:16 +02:00
|
|
|
typescript?: boolean
|
2020-05-10 23:51:47 +02:00
|
|
|
}): Promise<void> {
|
2020-02-27 16:32:33 +01:00
|
|
|
let repoInfo: RepoInfo | undefined
|
2021-05-07 10:08:16 +02:00
|
|
|
const template = typescript ? 'typescript' : 'default'
|
2020-02-27 16:32:33 +01:00
|
|
|
|
2019-07-19 21:55:30 +02:00
|
|
|
if (example) {
|
2020-02-27 16:32:33 +01:00
|
|
|
let repoUrl: URL | undefined
|
|
|
|
|
|
|
|
try {
|
|
|
|
repoUrl = new URL(example)
|
2021-09-16 18:06:57 +02:00
|
|
|
} catch (error: any) {
|
2020-02-27 16:32:33 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-04-06 15:57:19 +02:00
|
|
|
repoInfo = await getRepoInfo(repoUrl, examplePath)
|
2020-02-27 16:32:33 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2020-05-26 18:39:18 +02:00
|
|
|
} else if (example !== '__internal-testing-retry') {
|
2020-02-27 16:32:33 +01:00
|
|
|
const found = await hasExample(example)
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
console.error(
|
|
|
|
`Could not locate an example named ${chalk.red(
|
|
|
|
`"${example}"`
|
2021-07-07 13:12:58 +02:00
|
|
|
)}. 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.`
|
2020-02-27 16:32:33 +01:00
|
|
|
)
|
|
|
|
process.exit(1)
|
|
|
|
}
|
2019-07-19 21:55:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const root = path.resolve(appPath)
|
2020-08-19 19:09:34 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-07-19 21:55:30 +02:00
|
|
|
const appName = path.basename(root)
|
|
|
|
|
|
|
|
await makeDir(root)
|
|
|
|
if (!isFolderEmpty(root, appName)) {
|
|
|
|
process.exit(1)
|
|
|
|
}
|
|
|
|
|
2022-03-04 00:49:24 +01:00
|
|
|
const useYarn = packageManager === 'yarn'
|
2019-07-19 21:55:30 +02:00
|
|
|
const isOnline = !useYarn || (await getOnline())
|
|
|
|
const originalDirectory = process.cwd()
|
|
|
|
|
|
|
|
console.log(`Creating a new Next.js app in ${chalk.green(root)}.`)
|
|
|
|
console.log()
|
|
|
|
|
|
|
|
process.chdir(root)
|
|
|
|
|
|
|
|
if (example) {
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* If an example repository is provided, clone it.
|
|
|
|
*/
|
2020-05-26 18:39:18 +02:00
|
|
|
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) {
|
2021-09-16 18:06:57 +02:00
|
|
|
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 + ''
|
|
|
|
)
|
2020-02-27 16:32:33 +01:00
|
|
|
}
|
2019-11-11 06:42:51 +01:00
|
|
|
// Copy our default `.gitignore` if the application did not provide one
|
|
|
|
const ignorePath = path.join(root, '.gitignore')
|
|
|
|
if (!fs.existsSync(ignorePath)) {
|
|
|
|
fs.copyFileSync(
|
2021-05-07 10:08:16 +02:00
|
|
|
path.join(__dirname, 'templates', template, 'gitignore'),
|
2019-11-11 06:42:51 +01:00
|
|
|
ignorePath
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
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')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-07-19 21:55:30 +02:00
|
|
|
console.log('Installing packages. This might take a couple of minutes.')
|
|
|
|
console.log()
|
|
|
|
|
2022-03-04 00:49:24 +01:00
|
|
|
await install(root, null, { packageManager, isOnline })
|
2019-07-19 21:55:30 +02:00
|
|
|
console.log()
|
|
|
|
} else {
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* Otherwise, if an example repository is not provided for cloning, proceed
|
|
|
|
* by installing from a template.
|
|
|
|
*/
|
2022-03-04 00:49:24 +01:00
|
|
|
console.log(chalk.bold(`Using ${packageManager}.`))
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* Create a package.json for the new project.
|
|
|
|
*/
|
2019-07-19 21:55:30 +02:00
|
|
|
const packageJson = {
|
|
|
|
name: appName,
|
2022-02-06 22:03:35 +01:00
|
|
|
version: '0.1.0',
|
2019-07-19 21:55:30 +02:00
|
|
|
private: true,
|
2021-05-07 10:08:16 +02:00
|
|
|
scripts: {
|
|
|
|
dev: 'next dev',
|
|
|
|
build: 'next build',
|
|
|
|
start: 'next start',
|
2021-06-03 14:01:24 +02:00
|
|
|
lint: 'next lint',
|
2021-05-07 10:08:16 +02:00
|
|
|
},
|
2019-07-19 21:55:30 +02:00
|
|
|
}
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* Write it to disk.
|
|
|
|
*/
|
2019-07-19 21:55:30 +02:00
|
|
|
fs.writeFileSync(
|
|
|
|
path.join(root, 'package.json'),
|
|
|
|
JSON.stringify(packageJson, null, 2) + os.EOL
|
|
|
|
)
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* These flags will be passed to `install()`.
|
|
|
|
*/
|
2022-03-04 00:49:24 +01:00
|
|
|
const installFlags = { packageManager, isOnline }
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* Default dependencies.
|
|
|
|
*/
|
|
|
|
const dependencies = ['react', 'react-dom', 'next']
|
|
|
|
/**
|
|
|
|
* Default devDependencies.
|
|
|
|
*/
|
2021-11-16 10:18:27 +01:00
|
|
|
const devDependencies = ['eslint', 'eslint-config-next']
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* TypeScript projects will have type definitions and other devDependencies.
|
|
|
|
*/
|
|
|
|
if (typescript) {
|
2022-02-24 21:04:15 +01:00
|
|
|
devDependencies.push(
|
|
|
|
'typescript',
|
|
|
|
'@types/react',
|
|
|
|
'@types/node',
|
|
|
|
'@types/react-dom'
|
|
|
|
)
|
2021-05-07 10:08:16 +02:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* 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()
|
2019-07-19 21:55:30 +02:00
|
|
|
|
2021-05-07 10:08:16 +02:00
|
|
|
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()
|
2019-07-19 21:55:30 +02:00
|
|
|
|
2021-05-07 10:08:16 +02:00
|
|
|
const devInstallFlags = { devDependencies: true, ...installFlags }
|
|
|
|
await install(root, devDependencies, devInstallFlags)
|
|
|
|
}
|
2019-07-19 21:55:30 +02:00
|
|
|
console.log()
|
2021-05-07 10:08:16 +02:00
|
|
|
/**
|
|
|
|
* Copy the template files to the target directory.
|
|
|
|
*/
|
2019-07-19 21:55:30 +02:00
|
|
|
await cpy('**', root, {
|
|
|
|
parents: true,
|
2021-05-07 10:08:16 +02:00
|
|
|
cwd: path.join(__dirname, 'templates', template),
|
2020-05-18 21:24:37 +02:00
|
|
|
rename: (name) => {
|
2019-07-19 21:55:30 +02:00
|
|
|
switch (name) {
|
2021-06-03 14:01:24 +02:00
|
|
|
case 'gitignore':
|
2021-08-04 23:53:15 +02:00
|
|
|
case 'eslintrc.json': {
|
2019-07-19 21:55:30 +02:00
|
|
|
return '.'.concat(name)
|
|
|
|
}
|
2020-02-16 16:00:12 +01:00
|
|
|
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
|
2021-06-14 20:27:06 +02:00
|
|
|
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
|
2020-02-16 16:00:12 +01:00
|
|
|
case 'README-template.md': {
|
|
|
|
return 'README.md'
|
|
|
|
}
|
2019-07-19 21:55:30 +02:00
|
|
|
default: {
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-11-11 06:42:51 +01:00
|
|
|
if (tryGitInit(root)) {
|
|
|
|
console.log('Initialized a git repository.')
|
|
|
|
console.log()
|
|
|
|
}
|
|
|
|
|
2019-07-19 21:55:30 +02:00
|
|
|
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()
|
2022-03-04 00:49:24 +01:00
|
|
|
console.log(chalk.cyan(` ${packageManager} ${useYarn ? '' : 'run '}dev`))
|
2019-07-19 21:55:30 +02:00
|
|
|
console.log(' Starts the development server.')
|
|
|
|
console.log()
|
2022-03-04 00:49:24 +01:00
|
|
|
console.log(chalk.cyan(` ${packageManager} ${useYarn ? '' : 'run '}build`))
|
2019-07-19 21:55:30 +02:00
|
|
|
console.log(' Builds the app for production.')
|
|
|
|
console.log()
|
2022-03-04 00:49:24 +01:00
|
|
|
console.log(chalk.cyan(` ${packageManager} start`))
|
2019-07-19 21:55:30 +02:00
|
|
|
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(
|
2022-03-04 00:49:24 +01:00
|
|
|
` ${chalk.cyan(`${packageManager} ${useYarn ? '' : 'run '}dev`)}`
|
2019-07-19 21:55:30 +02:00
|
|
|
)
|
|
|
|
console.log()
|
|
|
|
}
|