gulpでPugのコンパイル環境構築

gulpでPugのコンパイル環境を構築します。

gulpのバージョンは4です。
node.jsやyarnのインストールについては省いています。
Pugの細かい説明は省いています。

やること
  1. Pugのコンパイル環境を作成
  2. pugタスクの自動実行
  3. watchタスクにキャッシュ機能を付ける
  4. 親子関係の解消
  5. 絶対パスを相対パスにする
目次

Pugのコンパイル環境を作成

ファイル構成

src/pug/をコンパイルしてpublic/に出力する構成です。

├─ public/
│   └─ コンパイルされたHTML
│
├─ src/
│   └─ pug/
│       ├─ index.pug
│       ├─ service/
│       │   └─ index.pug
│       │
│       └─ _include/
│           ├─ _config.pug
│           ├─ _head.pug
│           ├─ _js.pug
│           └─ _layout.pug
│
├─ gulpfile.js
└─ package.json

pugファイルの作成

src/pugディレクトリに以下のファイルを作成します。

index.pugトップページ
service/index.pugサービスページ
_includes/_config.pug設定ファイル
_includes/_head.pug共通パーツ <head>
_includes/_js.pug共通パーツ JS
_includes/_layout.pugレイアウト

動作確認を行うためのシンプルなファイル構成で、_includesディレクトリにはHTMLを出力しないファイルをまとめています。

//- 定数
- var SITE_URL = "https://localhost/";
- var SITE_NAME = "テスト";

//- 変数
- var lang = "ja";
- var description = "description";
- var ogType = "website";
- var ogUrl = SITE_URL + "assets/images/ogp.png";
meta(charset="UTF-8")

- title = (title ? title + " | " + SITE_NAME : SITE_NAME)
title #{title}

meta(name="description" content=description)

block css
  link(rel="stylesheet" href=SITE_URL + "assets/css/style.css")

//- OG
meta(property="og:title" content=(ogTitle ? ogTitle + " | " + SITE_NAME : title))
meta(property="og:type" content=ogType)
meta(property="og:image" content=ogImage)
meta(property="og:url" content=ogUrl)
meta(property="og:description" content=description)
meta(property="og:site_name" content=SITE_NAME)
meta(property="og:locale" content=lang)
meta(property="fb:admins" content="")
meta(property="fb:app_id" content="")
block js
  script(type="text/javascript" src=SITE_URL + "assets/js/common.min.js")
//- 設定読み込み
include _config

//- 設定の上書き
block variables

doctype html
html
  head
    //- head読み込み
    include _head

  body
    div.container
      main
        //- コンテンツ
        block content

    //- JS読み込み
    include _js
extend _includes/_layout

//- 設定の上書き
append variables
  //- Require
  - var title = 'ホーム';
  - var description = 'ディスクリプション';

  //- Optional
  //- var ogType = '';
  //- var ogTitle = '';
  //- var ogUrl = '';

//- CSS追加
append css
  link(rel="stylesheet" href=SITE_URL + "assets/css/layout-home.css")

//- JS追加
append js
  script(type="text/javascript" src=SITE_URL + "assets/js/home.min.js")

//- コンテンツ
block content
  p コンテンツ
extend ../_includes/_layout

//- 設定の上書き
append variables
  //- Require
  - var lang = 'en';
  - var title = 'サービス';
  - var description = 'サービスディスクリプション';

  //- Optional
  - var ogType = 'ogtest';
  - var ogTitle = 'ogtitle';
  - var ogUrl = 'ogurl';

//- コンテンツ
block content
  p サービス

パッケージの追加

gulpを実行するために必要なパッケージを入れます。

yarn add -D gulp gulp-notify gulp-plumber gulp-debug

gulp-notifyはエラー通知。
gulp-plumberはエラーが発生した場合にタスクが停止するのを防止します。
gulp-debugは処理中のファイルをログに表示できます。

次はPugをコンパイルするために必要なパッケージを追加します。

yarn add -D gulp-pug gulp-html-beautify gulp-filter

gulp-pugは今回の主役です。
gulp-html-beautifyは出力するHTMLを綺麗に整形できます。
gulp-filter_から始まるファイル(前述の_include/_config.pugなど)をコンパイル対象外にするために使用します。gulp.src()関数でも同じことができますが、後に設定する親子関係を解消するところで必須になるので先に入れておきます。

gulpfile.jsの作成と実行

gulpfile.jsにPugのコンパイルタスクを記述します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(pug())
    .pipe(htmlbeautify({
      eol: '\n',
      indent_size: 2,
      indent_char: ' ',
      indent_with_tabs: false,
      end_with_newline: true,
      preserve_newlines: true,
      max_preserve_newlines: 2,
      indent_inner_html: true,
      brace_style: 'collapse',
      indent_scripts: 'normal',
      wrap_line_length: 0,
      wrap_attributes: 'auto'
    }))
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

exports.pug = pugCompile; // pugタスク
exports.default = gulp.series(pugCompile); // defaultタスク

HTMLを整形するhtmlbeautifyのオプションは好みなので調整が必要かもしれません。

作成したタスクが正常に動作するか確認するため、ターミナルからyarn gulpyarn gulp pug、環境によってはgulpgulp pugでコンパイルできるか試してみます。

# defaultタスクの実行
yarn gulp

# pugタスクの実行
yarn gulp pug

正常に動作した場合は以下のファイルが出力されます。

  • public/index.html
  • public/service/index.html

これでPugのコンパイル環境は完成です。

ここから先はhtmlbeautifyのオプションが長いので削除しています

pugタスクの自動実行

前述で作成したpugタスクを自動実行させるためにwatchタスクを作成します。
watchタスクはファイルを監視して、変更があった場合に指定したタスクを実行できます。

watchタスクの追加と実行

gulpfile.jsを以下のように変更します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(pug())
    .pipe(htmlbeautify())
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

/**
 * watchタスクで実行する関数
 */
function watch() {
  return gulp.watch(paths.pug.src, gulp.series(pugCompile))
}

exports.pug = pugCompile; // pugタスク
exports.watch = gulp.series(watch); // watchタスク
exports.default = gulp.series(pugCompile); // defaultタスク

watchタスクで実行する関数(34-39行目)とwatchタスク(43行目)を追加しました。
監視対象はsrc/pug/**/*.pugです。

ターミナルからyarn gulp watchコマンドを実行すると監視状態になります。

# watchタスクの実行(停止:Ctrl + C)
yarn gulp watch

監視状態でsrc/pug/index.pugsrc/pug/service/index.pugを変更して保存すると自動的にコンパイルされます。

ですが、どのファイルを変更しても全てのファイルがコンパイルされるので、次はキャッシュ機能を付けて、変更があったファイルのみコンパイルされるようにします。

watchタスクにキャッシュ機能を付ける

キャッシュ機能を付けるにはgulp-cachedパッケージを使用します。
また、キャッシュ機能はwatchタスクの時だけ動いてほしいので、条件分岐ができるgulp-if パッケージも入れます。

パッケージの追加と実行

ターミナルから以下のコマンドを実行してパッケージを追加します。

yarn add -D gulp-cached gulp-if

gulpfile.jsを以下のように変更します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター
const cached = require('gulp-cached'); // ファイルキャッシュ
const gulpif = require('gulp-if'); // 条件分岐

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

let isWatching = false; // watchタスクを動かしているか

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(gulpif(isWatching, cached('pug'))) // watchタスク時にキャッシュ機能を使う
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(pug())
    .pipe(htmlbeautify())
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

/**
 * watchタスクで実行する関数
 */
function watch() {
  isWatching = true;
  return gulp.watch(paths.pug.src, gulp.series(pugCompile))
}

exports.pug = pugCompile; // pugタスク
exports.watch = gulp.series(watch); // watchタスク
exports.default = gulp.series(pugCompile); // defaultタスク

7,8行目でgulp-cachedgulp-ifパッケージを読み込み、
19行目にwatchタスクの有無を判定するisWatching変数を追加をしました。

watchタスク実行時に43行目でisWatching = trueを設定し、
29行目のgulpifisWatching == trueのときだけキャッシュ機能を使うようにしています。

ターミナルからyarn gulp watchコマンドを実行して、src/pug/index.pugsrc/pug/service/index.pugを変更すると、1回目はキャッシュがないので全てのファイルがコンパイルされ、2回目からは変更したファイルだけコンパイルされるようになります。

処理説明をすると実際はすべてのファイルが処理されていますが、キャッシュが行われたファイルに変更がない場合は29行目で処理が中断されます。

これでキャッシュ機能が正常に動いているのは確認できましたが、できれば1回目から変更したファイルだけコンパイルするのが理想的なので、先にキャッシュさせるように変更します。

先にキャッシュを行うように変更

gulpfile.jsを以下のように変更します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター
const cached = require('gulp-cached'); // ファイルキャッシュ
const gulpif = require('gulp-if'); // 条件分岐

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

let isWatching = false; // watchタスクを動かしているか

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(gulpif(isWatching, cached('pug'))) // watchタスク時にキャッシュ機能を使う
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(pug())
    .pipe(htmlbeautify())
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

/**
 * キャッシュ
 */
function pugCache(){
  return gulp.src(paths.pug.src)
    .pipe(cached('pug'))
    .pipe(debug({title: 'pug cached:'}));
}

/**
 * watchタスクで実行する関数
 */
function watch() {
  isWatching = true;
  return gulp.watch(paths.pug.src, gulp.series(pugCompile))
}

exports.pug = pugCompile; // pugタスク
exports.watch = gulp.series(pugCache, watch); // watchタスク
exports.default = gulp.series(pugCompile); // defaultタスク

39-46行目にpugファイルのキャッシュを行うpugCache関数を追加し、57行目のwatchタスクでpugCache関数を呼び出した後にwatch関数を呼ぶように変更しました。

ターミナルからyarn gulp watchコマンドを実行してsrc/pug/index.pugsrc/pug/service/index.pugを変更して保存すると、対象のファイルだけコンパイルされるようになります。

ですが、src/pug/_includes/_layout.pugを変更して保存するとどうでしょうか?コンパイルされないはずです。
これは30-32行目で設定しているgulp-filterで、ファイル名が_から始まるファイルはコンパイル対象外になるためです。

次はこの問題を解決します。

親子関係の解決

先にPugの構成を整理すると、
src/pug/index.pug の1行目はextend _includes/_layout となっていて、src/pug/_includes/_layout.pugを読み込んでいます。

読み込んでいる方を「子」にすると以下のようになります。

親 =src/pug/index.pug
子 =src/pug/_includes/_layout.pug

以下の作業を行えば、子に変更があると親のファイルがコンパイルされるようになります。

@unisharp/gulp-pug-inheritanceパッケージの追加と実行

ターミナルから以下のコマンドを実行してパッケージを追加します。

yarn add -D @unisharp/gulp-pug-inheritance

gulpfile.jsを以下のように変更します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター
const cached = require('gulp-cached'); // ファイルキャッシュ
const gulpif = require('gulp-if'); // 条件分岐

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形
const pugInheritance = require('@unisharp/gulp-pug-inheritance'); // 親子関係を解決

const path = require('path');

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

let isWatching = false; // watchタスクを動かしているか

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(gulpif(isWatching, cached('pug'))) // watchタスク時にファイルをキャッシュさせる
    .pipe(gulpif(isWatching, pugInheritance(paths.pug.src))) // 親子関係を解決
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(pug())
    .pipe(htmlbeautify())
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

/**
 * キャッシュ
 */
function pugCache(){
  return gulp.src(paths.pug.src)
    .pipe(cached('pug'))
    .pipe(debug({title: 'pug cached:'}));
}

/**
 * watchタスクで実行する関数
 */
function watch() {
  isWatching = true;
  return gulp.watch(paths.pug.src, gulp.series(pugCompile))
}

exports.pug = pugCompile; // pugタスク
exports.watch = gulp.series(pugCache, watch); // watchタスク
exports.default = gulp.series(pugCompile); // defaultタスク

11, 33行目を追記しました。
28行目のpugInheritance 関数の引数には、すべてのpugファイルを管理させる必要があるのでpaths.pug.src (src/pug/**/*.pug)を指定しています。また、watchタスクのみで動作させるようにしています。

ターミナルからyarn gulp watchコマンドを実行してsrc/pug/_includes/_layout.pug を変更すると、親になっているsrc/pug/index.pugsrc/pug/service/index.pugがコンパイルされます。

細かく動作確認するなら、src/pug/index.pug の1行目をコメントアウトしてsrc/pug/_includes/_layout.pug を変更すると、src/pug/service/index.pugだけコンパイルされるはずです。

これでwatchタスクは完璧です。

最後は出力されたHTMLのscriptパスなどが絶対パスになっている部分の修正です。

絶対パスを相対パスにする

絶対パスを相対パスにする方法はいくつかあると思いますが、今回はgulpからpugにデータを渡して処理させる方法を使います。

gulpfile.jsで実行しているpugのオプションにlocalsというオプションがあり、このlocalsに値を入れるとpugファイル内で使用することができます。

実装イメージは以下のようになります。

pug({
  locals:{
    キー: 値
  }
})

このlocalsに処理中のpugファイルがルートから何階層目にあるか解析した結果を入れます。
ですが、そのまま書くと少し関数が長くなり可読性が落ちるためgulp-dataというパッケージを使います。

このパッケージを使って処理されたデータは、pugが自動でlocalsにマージします。

gulp-dataパッケージの追加と実行

ターミナルから以下のコマンドを実行してパッケージを追加します。

yarn add -D gulp-data

gulpfile.jsを以下のように変更します。

const gulp = require('gulp');
const notify = require('gulp-notify');  // エラー通知
const plumber = require('gulp-plumber'); // エラー時のタスク停止防止
const debug = require('gulp-debug'); // ログ表示
const filter = require('gulp-filter'); // ファイルフィルター
const cached = require('gulp-cached'); // ファイルキャッシュ
const gulpif = require('gulp-if'); // 条件分岐
const data = require('gulp-data'); // データオブジェクトの作成

const pug = require('gulp-pug'); // Pug
const htmlbeautify = require('gulp-html-beautify'); // HTML整形
const pugInheritance = require('@unisharp/gulp-pug-inheritance'); // 親子関係を解決

const path = require('path');

const paths = {
  pug: {
    src: 'src/pug/**/*.pug', // コンパイル対象
    dest: 'public/' // 出力先
  }
}

let isWatching = false; // watchタスクを動かしているか

/**
 * pugタスク
 */
function pugCompile() {
  return gulp.src(paths.pug.src)
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(gulpif(isWatching, cached('pug'))) // watchタスク時にファイルをキャッシュさせる
    .pipe(gulpif(isWatching, pugInheritance(paths.pug.src))) // 親子関係を解決
    .pipe(filter(function (file) { // _から始まるファイルを除外
      return !/\/_/.test(file.path) && !/^_/.test(file.relative);
    }))
    .pipe(data(file => { // データオブジェクトの作成
      return {
        hierarchy: path.relative(file.relative, '.').replace(/\.\.$/, '') || './' // 相対階層
      }
    }))
    .pipe(pug())
    .pipe(htmlbeautify({
      eol: '\n',
      indent_size: 2,
      indent_char: ' ',
      indent_with_tabs: false,
      end_with_newline: true,
      preserve_newlines: true,
      max_preserve_newlines: 2,
      indent_inner_html: true,
      brace_style: 'collapse',
      indent_scripts: 'normal',
      wrap_line_length: 0,
      wrap_attributes: 'auto'
    }))
    .pipe(gulp.dest(paths.pug.dest))
    .pipe(debug({title: 'pug dest:'}));
}

/**
 * キャッシュ
 */
function pugCache(){
  return gulp.src(paths.pug.src)
    .pipe(cached('pug'))
    .pipe(debug({title: 'pug cached:'}));
}

/**
 * watchタスクで実行する関数
 */
function watch() {
  isWatching = true;
  return gulp.watch(paths.pug.src, gulp.series(pugCompile))
}

exports.pug = pugCompile; // pugタスク
exports.watch = gulp.series(pugCache, watch); // watchタスク
exports.default = gulp.series(pugCompile); // defaultタスク

8行目にgulp-dataを追加し、
38-42行目に相対パスの階層./ ../などをhierarchy プロパティに入れるようにしました。

次はこのheierarchyプロパティをpugで処理します。

src/pug/_includes/_config.pug に関数を追加します。

//- 定数
- var SITE_URL = "https://localhost/";
- var SITE_NAME = "テスト";

//- 変数
- var lang = "ja";
- var description = "description";
- var ogType = "website";
- var ogUrl = SITE_URL + "assets/images/ogp.png";

//- 相対パス
- function relativeUrl(url){ return hierarchy + url.replace(/(^\/)/, ''); }

11-12行目にrelativeUrl関数を追加しました。

全体で使用しているpugを編集すると結果が分かりやすいので、src/pug/_includes/_js.pugを以下のように編集します。

block js
  script(type="text/javascript" src=relativeUrl("assets/js/common.min.js"))

これでターミナルからyarn gulpをして、出力されたHTMLファイルのscript部分を確認してみてください。
public/index.htmlではsrc="./assets/js/common.min.js"
public/service/index.html ではsrc="../assets/js/common.min.js"
になったでしょうか?

あとは同じ要領で他の部分も変更して完了です。

終わりに

gulpを使ったPugのコンパイル環境構築はできたでしょうか。
ファイル構成に関してベストとは言えませんが、今回の構成でもそれなりに使用できるので拡張してもらえたらと思います。

今回作成したファイルは以下からダウンロードできます。

  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次