なんで読んでみたか
私は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()
}
}
まず気になったのは前半にある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について勉強したいと感じました。
全然よく分かってなくて申し訳ないですが、これでこの記事は終了とさせて頂きます。