Help us understand the problem. What is going on with this article?

create-nuxt-appを読んでみた

More than 1 year has passed since last update.

なんで読んでみたか

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

ソースコードはGitHubにアップされていました。

create-nuxt-appとは

Nuxt.jsのインストールガイドにも記載されている、クイックスタート用のテンプレートです。

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

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

npx create-nuxt-app <project-name>

cli.js

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

#!/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よりは短くはありませんが、インタラクティブに使用するフレームワークを決定するためと考えられるコードが見られ、理解はそれほど難しくなさそうです。

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()
  }
}

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

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

これらの関数はmodule.exportsmove(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について勉強したいと感じました。

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away