Repository: gatsbyjs/gatsby
File: packages/gatsby-cli/src/init-starter.ts (TypeScript)

Code before:
import opn from "better-opn"
import { execSync } from "child_process"
import execa from "execa"
import { sync as existsSync } from "fs-exists-cached"
import fs from "fs-extra"
import { trackCli, trackError } from "gatsby-telemetry"
import hostedGitInfo from "hosted-git-info"
import isValid from "is-valid-path"
import sysPath from "path"
import prompts from "prompts"
import url from "url"

import report from "./reporter"
import { getPackageManager, promptPackageManager } from "./util/package-manager"
import { isTTY } from "./util/is-tty"

const spawnWithArgs = (
  file: string,
  args: string[],
  options?: execa.Options
): execa.ExecaChildProcess =>
  execa(file, args, { stdio: `inherit`, preferLocal: false, ...options })

const spawn = (
  cmd: string,
  options?: execa.Options
): execa.ExecaChildProcess => {
  const [file, ...args] = cmd.split(/\s+/)
  return spawnWithArgs(file, args, options)
}
// Checks the existence of yarn package and user preference if it exists
// We use yarnpkg instead of yarn to avoid conflict with Hadoop yarn
// Refer to https://github.com/yarnpkg/yarn/issues/673
const shouldUseYarn = async (): Promise<boolean> => {
  try {
    execSync(`yarnpkg --version`, { stdio: `ignore` })

    let packageManager = getPackageManager()
    if (!packageManager) {
      // if package manager is not set:
      //  - prompt user to pick package manager if in interactive console
      //  - default to yarn if not in interactive console
      if (isTTY()) {
        packageManager = (await promptPackageManager()) || `yarn`
      } else {
        packageManager = `yarn`
      }
    }

    return packageManager === `yarn`
  } catch (e) {
    return false
  }
}

const isAlreadyGitRepository = async (): Promise<boolean> => {
  try {
    return await spawn(`git rev-parse --is-inside-work-tree`, {
      stdio: `pipe`,
    }).then(output => output.stdout === `true`)
  } catch (err) {
    return false
  }
}

// Initialize newly cloned directory as a git repo
const gitInit = async (
  rootPath: string
): Promise<execa.ExecaReturnBase<string>> => {
  report.info(`Initialising git in ${rootPath}`)

  return await spawn(`git init`, { cwd: rootPath })
}

// Create a .gitignore file if it is missing in the new directory
const maybeCreateGitIgnore = async (rootPath: string): Promise<void> => {
  if (existsSync(sysPath.join(rootPath, `.gitignore`))) {
    return
  }

  report.info(`Creating minimal .gitignore in ${rootPath}`)
  await fs.writeFile(
    sysPath.join(rootPath, `.gitignore`),
    `.cache\nnode_modules\npublic\n`
  )
}

// Create an initial git commit in the new directory
const createInitialGitCommit = async (
  rootPath: string,
  starterUrl: string
): Promise<void> => {
  report.info(`Create initial git commit in ${rootPath}`)

  await spawn(`git add -A`, { cwd: rootPath })
  // use execSync instead of spawn to handle git clients using
  // pgp signatures (with password)
  try {
    execSync(`git commit -m "Initial commit from gatsby: (${starterUrl})"`, {
      cwd: rootPath,
    })
  } catch {
    // Remove git support if initial commit fails
    report.info(`Initial git commit failed - removing git support\n`)
    fs.removeSync(sysPath.join(rootPath, `.git`))
  }
}

// Executes `npm install` or `yarn install` in rootPath.
const install = async (rootPath: string): Promise<void> => {
  const prevDir = process.cwd()

  report.info(`Installing packages...`)
  process.chdir(rootPath)

  try {
    if (await shouldUseYarn()) {
      await fs.remove(`package-lock.json`)
      await spawn(`yarnpkg`)
    } else {
      await fs.remove(`yarn.lock`)
      await spawn(`npm install`)
    }
  } finally {
    process.chdir(prevDir)
  }
}

const ignored = (path: string): boolean =>
  !/^\.(git|hg)$/.test(sysPath.basename(path))

// Copy starter from file system.
const copy = async (
  starterPath: string,
  rootPath: string
): Promise<boolean> => {
  // Chmod with 755.
  // 493 = parseInt('755', 8)
  await fs.ensureDir(rootPath, { mode: 493 })

  if (!existsSync(starterPath)) {
    throw new Error(`starter ${starterPath} doesn't exist`)
  }

  if (starterPath === `.`) {
    throw new Error(
      `You can't create a starter from the existing directory. If you want to
      create a new site in the current directory, the trailing dot isn't
      necessary. If you want to create a new site from a local starter, run
      something like "gatsby new new-gatsby-site ../my-gatsby-starter"`
    )
  }

  report.info(`Creating new site from local starter: ${starterPath}`)

  report.log(`Copying local starter to ${rootPath} ...`)

  await fs.copy(starterPath, rootPath, { filter: ignored })

  report.success(`Created starter directory layout`)

  await install(rootPath)

  return true
}

// Clones starter from URI.
const clone = async (
  hostInfo: hostedGitInfo,
  rootPath: string
): Promise<void> => {
  let url: string
  // Let people use private repos accessed over SSH.
  if (hostInfo.getDefaultRepresentation() === `sshurl`) {
    url = hostInfo.ssh({ noCommittish: true })
    // Otherwise default to normal git syntax.
  } else {
    url = hostInfo.https({ noCommittish: true, noGitPlus: true })
  }

  const branch = hostInfo.committish ? [`-b`, hostInfo.committish] : []

  report.info(`Creating new site from git: ${url}`)

  const args = [
    `clone`,
    ...branch,
    url,
    rootPath,
    `--recursive`,
    `--depth=1`,
  ].filter(arg => Boolean(arg))

  await spawnWithArgs(`git`, args)

  report.success(`Created starter directory layout`)

  await fs.remove(sysPath.join(rootPath, `.git`))

  await install(rootPath)
  const isGit = await isAlreadyGitRepository()
  if (!isGit) await gitInit(rootPath)
  await maybeCreateGitIgnore(rootPath)
  if (!isGit) await createInitialGitCommit(rootPath, url)
}

interface IGetPaths {
  starterPath: string
  rootPath: string
  selectedOtherStarter: boolean
}

const getPaths = async (
  starterPath?: string,
  rootPath?: string
): Promise<IGetPaths> => {
  let selectedOtherStarter = false

  // if no args are passed, prompt user for path and starter
  if (!starterPath && !rootPath) {
    const response = await prompts.prompt([
      {
        type: `text`,
        name: `path`,
        message: `What is your project called?`,
        initial: `my-gatsby-project`,
      },
      {
        type: `select`,
        name: `starter`,
        message: `What starter would you like to use?`,
        choices: [
          { title: `gatsby-starter-default`, value: `gatsby-starter-default` },
          {
            title: `gatsby-starter-hello-world`,
            value: `gatsby-starter-hello-world`,
          },
          { title: `gatsby-starter-blog`, value: `gatsby-starter-blog` },
          { title: `(Use a different starter)`, value: `different` },
        ],
        initial: 0,
      },
    ])
    // exit gracefully if responses aren't provided
    if (!response.starter || !response.path.trim()) {
      throw new Error(
        `Please mention both starter package and project name along with path(if its not in the root)`
      )
    }

    selectedOtherStarter = response.starter === `different`
    starterPath = `gatsbyjs/${response.starter}`
    rootPath = response.path
  }

  // set defaults if no root or starter has been set yet
  rootPath = rootPath || process.cwd()
  starterPath = starterPath || `gatsbyjs/gatsby-starter-default`

  return { starterPath, rootPath, selectedOtherStarter }
}

const successMessage = (path: string): void => {
  report.info(`
Your new Gatsby site has been successfully bootstrapped. Start developing it by running:

  cd ${path}
  gatsby develop
`)
}

/**
 * Main function that clones or copies the starter.
 */
export async function initStarter(
  starter?: string,
  root?: string
): Promise<void> {
  const { starterPath, rootPath, selectedOtherStarter } = await getPaths(
    starter,
    root
  )

  const urlObject = url.parse(rootPath)

  if (selectedOtherStarter) {
    report.info(
      `Opening the starter library at https://gatsby.dev/starters?v=2...\nThe starter library has a variety of options for starters you can browse\n\nYou can then use the gatsby new command with the link to a repository of a starter you'd like to use, for example:\ngatsby new ${rootPath} https://github.com/gatsbyjs/gatsby-starter-default`
    )
    opn(`https://gatsby.dev/starters?v=2`)
    return
  }
  if (urlObject.protocol && urlObject.host) {
    trackError(`NEW_PROJECT_NAME_MISSING`)

    const isStarterAUrl =
      starter && !url.parse(starter).hostname && !url.parse(starter).protocol

    if (/gatsby-starter/gi.test(rootPath) && isStarterAUrl) {
      report.panic({
        id: `11610`,
        context: {
          starter,
          rootPath,
        },
      })
      return
    }
    report.panic({
      id: `11611`,
      context: {
        rootPath,
      },
    })
    return
  }

  if (!isValid(rootPath)) {
    report.panic({
      id: `11612`,
      context: {
        path: sysPath.resolve(rootPath),
      },
    })
    return
  }

  if (existsSync(sysPath.join(rootPath, `package.json`))) {
    trackError(`NEW_PROJECT_IS_NPM_PROJECT`)
    report.panic({
      id: `11613`,
      context: {
        rootPath,
      },
    })
    return
  }

  const hostedInfo = hostedGitInfo.fromUrl(starterPath)

  trackCli(`NEW_PROJECT`, {
    starterName: hostedInfo ? hostedInfo.shortcut() : `local:starter`,
  })
  if (hostedInfo) await clone(hostedInfo, rootPath)
  else await copy(starterPath, rootPath)
  successMessage(rootPath)
  trackCli(`NEW_PROJECT_END`)
}


Review comment (refactor):
Could we move this to a function so it's more clear what this does and why we need it? Maybe worth to move it into core-utils?

```js
/**
 * the metadata file is used in gatsby desktop to discover which projects are created for gatsby
 */
function saveProjectToMetadataFile() {}
```

Code after:
  const hostedInfo = hostedGitInfo.fromUrl(starterPath)

  trackCli(`NEW_PROJECT`, {
    starterName: hostedInfo ? hostedInfo.shortcut() : `local:starter`,
  })
  if (hostedInfo) {
    await clone(hostedInfo, rootPath)
  } else {
    await copy(starterPath, rootPath)
  }

  const sitePath = sysPath.resolve(rootPath)

  const sitePackageJson = await fs
    .readJSON(sysPath.join(sitePath, `package.json`))
    .catch(() => {
      reporter.verbose(
        `Could not read "${sysPath.join(sitePath, `package.json`)}"`
      )
    })

  await createServiceLock(sitePath, `metadata`, {
    name: sitePackageJson?.name || rootPath,
    sitePath,
    lastRun: Date.now(),
  }).then(unlock => unlock?.())

  successMessage(rootPath)
  trackCli(`NEW_PROJECT_END`)
}