create-nuxt-appを読んでみた

## なんで読んでみたか

私はJavaScript初心者で、Node.jsが全く分からないので、小さいNode.jsアプリケーションを読んでみたいなと思いました。Nuxt.jsを触る時にお世話になっている`nuxt/create-nuxt-app` を読んでみようと思いました。

ソースコードは[GitHub](https://github.com/nuxt/create-nuxt-app)にアップされていました。

## create-nuxt-appとは

[Nuxt.jsのインストールガイド](https://ja.nuxtjs.org/guide/installation/)にも記載されている、クイックスタート用のテンプレートです。

> 素早くスタートできるようにするため、Nuxt.js チームは足場ツール create-nuxt-app を作成しました。

`npx`(もしくは`yarn`)を使って実行します。

```
npx create-nuxt-app <project-name>
```

## `cli.js`

`cli.js`と名付けられたファイルは恐らくユーザーが触れている部分で分かりやすいだろうと考え、まずは`cli.js`を読んでみることにしました。開いてみると、非常に小さいファイルでした。

```javascript
#!/usr/bin/env node
const path = require('path')
const sao = require('sao')
const minimist = require('minimist')

const argv = minimist(process.argv.slice(2))
// In a custom directory or current directory
const targetPath = path.resolve(argv._[0] || '.')

console.log(`> Generating Nuxt.js project in ${targetPath}`)

// See https://sao.js.org/#/advanced/standalone-cli
sao({
  template: __dirname,
  targetPath
}).catch(err => {
  console.error(err.name === 'SAOError' ? err.message : err.stack)
  process.exit(1)
})
```

最初の3行で`path` `sao` `minimist`が要求されていることが分かります。その後のソースコードから、`minimist`はコマンドの引数をパースに関係するものと推測でき、`path`はパスの解決をしていることが分かります。

いずれもCLIを提供するならほぼ確実に必要なことなので、Node.jsでなにかツールを作るのであれば、ここに書かれているコードと同じようなコードを書く機会は多そうです。

気になったのは`sao`とはなんだろうか、という点です。コメントに書かれているURLは現在ではリンクが切れているようなので、一旦無視して同一ディレクトリ内にある`sao.js`というファイルを覗いてみることにします。

また、`sao`に渡されている引数に`template`と書いてあって、リポジトリのルートディレクトリに`template/`というディレクトリがあるので、そちらも気になります。`sao.js` `template/`の順に覗いていきます。

## `sao.js`

`sao.js`は以下のようになっていました。`cli.js`よりは短くはありませんが、インタラクティブに使用するフレームワークを決定するためと考えられるコードが見られ、理解はそれほど難しくなさそうです。

```javascript 
const superb = require('superb')
const glob = require('glob')
const { join } = require('path')
const spawn = require('cross-spawn')

const rootDir = __dirname

const move = (from, to = '') => {
  const result = {}
  const options = { cwd: join(rootDir, 'template'), nodir: true, dot: true }
  for (const file of glob.sync(`${from}/**`, options)) {
    result[file] = (to ? to + '/' : '') + file.replace(`${from}/`, '')
  }
  return result
}

const moveFramework = (answer, to = '') => {
  return answer !== 'none' && move(`frameworks/${answer}`, to);
}

module.exports = {
  prompts: {
    name: {
      message: 'Project name',
      default: ':folderName:'
    },
    description: {
      message: 'Project description',
      default: `My ${superb()} Nuxt.js project`
    },
    server: {
      message: 'Use a custom server framework',
      type: 'list',
      choices: [
        'none',
        'express',
        'koa',
        'adonis',
        'hapi',
        'feathers',
        'micro'
      ],
      default: 'none'
    },
    ui: {
      message: 'Use a custom UI framework',
      type: 'list',
      choices: [
        'none',
        'bootstrap',
        'vuetify',
        'bulma',
        'tailwind',
        'element-ui',
        'buefy',
        'ant-design-vue',
        'iview'
      ],
      default: 'none'
    },
    mode: {
      message: 'Choose rendering mode',
      type: 'list',
      choices: [
        { name: 'Universal', value: 'universal' },
        { name: 'Single Page App', value: 'spa' }
      ],
      default: 'universal'
    },
    axios: {
      message: 'Use axios module',
      type: 'list',
      choices: ['no', 'yes'],
      default: 'no'
    },
    eslint: {
      message: 'Use eslint',
      type: 'list',
      choices: ['no', 'yes'],
      default: 'no'
    },
    prettier: {
      message: 'Use prettier',
      type: 'list',
      choices: ['no', 'yes'],
      default: 'no'
    },
    author: {
      type: 'string',
      message: 'Author name',
      default: ':gitUser:',
      store: true
    },
    pm: {
      message: 'Choose a package manager',
      choices: ['npm', 'yarn'],
      type: 'list',
      default: 'npm'
    }
  },
  data: {
    edge: process.argv.includes('--edge')
  },
  filters: {
    'server/index-express.js': 'server === "express"',
    'server/index-koa.js': 'server === "koa"',
    'server/index-adonis.js': 'server === "adonis"',
    'server/index-hapi.js': 'server === "hapi"',
    'server/index-feathers.js': 'server === "feathers"',
    'server/index-micro.js': 'server === "micro"',
    'frameworks/adonis/**': 'server === "adonis"',
    'frameworks/feathers/**': 'server === "feathers"',
    'frameworks/vuetify/**': 'ui === "vuetify"',
    'frameworks/element-ui/**': 'ui === "element-ui"',
    'frameworks/ant-design-vue/**': 'ui === "ant-design-vue"',
    'frameworks/tailwind/**': 'ui === "tailwind"',
    'frameworks/buefy/**': 'ui === "buefy"',
    'frameworks/iview/**': 'ui === "iview"',
    '_.eslintrc.js': 'eslint === "yes"',
    '.prettierrc': 'prettier === "yes"'
  },
  move(answers) {
    const moveable = {
      gitignore: '.gitignore',
      '_package.json': 'package.json',
      '_.eslintrc.js': '.eslintrc.js',
      'server/index-*.js': 'server/index.js'
    }
    let nuxtDir
    if (answers.server === 'adonis') {
      nuxtDir = 'resources'
    }
    return Object.assign(
      moveable,
      move('nuxt', nuxtDir),
      moveFramework(answers.server),
      moveFramework(answers.ui, nuxtDir),
      answers.server === 'adonis'
        ? {
            'server/index-*.js': 'server.js',
            'nuxt/nuxt.config.js': 'config/nuxt.js'
          }
        : null
    )
  },
  post(
    { npmInstall, yarnInstall, gitInit, chalk, isNewFolder, folderName, folderPath },
    { meta }
  ) {
    gitInit()

    // using yarn or npm
    meta.answers.pm === 'yarn' ? yarnInstall() : npmInstall()

    const cd = () => {
      if (isNewFolder) {
        console.log(`\t${chalk.cyan('cd')} ${folderName}`)
      }
    }
    if (meta.answers.eslint === 'yes') {
      spawn.sync(meta.answers.pm, ['run','lint', '--', '--fix'], {
        cwd: folderPath,
        stdio: 'inherit'
      })
    }

    console.log()
    console.log(chalk.bold(`\tTo get started:\n`))
    cd()
    console.log(`\t ${meta.answers.pm} run dev\n`)
    console.log(chalk.bold(`  To build & start for production:\n`))
    cd()
    console.log(`\t ${meta.answers.pm} run build`)
    console.log(`\t ${meta.answers.pm} start`)
    console.log()
  }
}
```

まず気になったのは前半にある`move`と`moveFramework`で、どうやらテンプレートやフレームワークが、これらの関数によって展開されているようだと読み取れました。

その際に`glob`というモジュールが使用されていて、Node.jsでファイルを操作する際に便利なものなんだなということが分かりました。

これらの関数は`module.exports`の`move(answers)`内の`return Object.assign`で呼び出されているようで、全体を見ると、このコードはインタラクティブに使用するフレームワークを決定したあと、それを展開するためのコードなのだと推測できます。

## `template/`

`template/nuxt/`には、`create-nuxt-app`を実行直後に作成されるプロジェクト用のディレクトリ直下にある構造がそのまま置いてありました。`sao.js`と合わせて見ると、ここから展開していることが分かります。

`template/server/`及び`template/frameworks/`にはCLIで選択できる各種ツールに関するコードとみられるものが入っていました。

## わかったこと

`create-nuxt-app`はどうやらシンプルにテンプレートを展開するもののようです。

展開するテンプレートと展開するためのコード、テストのディレクトリ、依存解決のためのファイル、CIのためのディレクトリ、Git関連のファイルくらいしか見当たりませんでした。

どうやら余計なことはしていないので、心おきなく使えそうです。

また、Node.jsでCLIツールを作る際にこのコードは参考になりそうだなと感じました。

## よくわかっていないところ

JavaScript、Node.jsが全く分からない初心者なので、`module.exports`の構造が何を表しているのか、`require`がどのように要求を実行するのかなどが分かりませんでした。また、`npx`がこれをどう実行しているのかについてもっと知っておきたいと感じました。

また、`create-nuxt-app`で選択出来る各種ツールのことがいまいちよく分かってないです。もっとNuxt.jsについて勉強したいと感じました。

全然よく分かってなくて申し訳ないですが、これでこの記事は終了とさせて頂きます。