Riot.js v4 HTMLのテンプレートエンジンにPugを使う

[Riot.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/riotjs) の12日目が空いていたので埋めます。

## @riotjs/compiler

[@riotjs/compiler](https://github.com/riot/compiler)では`template`として登録した内容でコードを変更することが出来ます。
これを使って[Pug](https://pugjs.org/)をHTMLのテンプレートエンジンにすることができます。

Pugは`npm i -D pug`でインストール。

## @riotjs/compilerでの指定

@riotjs/compilerの`README.md`にある通り、`registerPreprocessor`でテンプレートを登録してから`compile`のときにその名前を指定するようです。

```javascript
// process your tag template before it will be compiled
registerPreprocessor('template', 'pug', function(code, { options }) {
  const { file } = options
  console.log('your file path is:', file)
  
  return {
    code: pug.render(code),
    // no sourcemap here
    map: null
  }
})
```
```javascript
const { code, map } = compile('<p>{hello}</p>', {
  // specify the template preprocessor
  template: 'pug'
})
```


## @riotjs/webpack-loaderでの指定

[@riotjs/webpack-loader](https://github.com/riot/webpack-loader)からこれをどうやって使うのかが分からなかったので[ソース](https://github.com/riot/webpack-loader/blob/master/src/index.js)を見ました。

```javascript:/riot/webpack-loader/blob/master/src/index.js
  // compile and generate sourcemaps
  const {code, map} = compile(
    source,
    {
      ...opts,
      file: this.resourcePath
    }
  )
```
どうやら`opts`に指定したものは全部@riotjs/compilerに渡してくれるようです。

というわけで、`webpack.config.js`で指定します。

```javascript:webpack.config.js
const path = require('path');
const compiler = require('@riotjs/compiler');
const pug = require('pug');

// pug のプリプロセッサを登録
compiler.registerPreprocessor('template', 'pug', (code, options) => {
  const { file } = options;

  return {
    code: pug.render(code, {
      filename: file,
      pretty: true,
      doctype: 'html'
    })
  };
});

module.exports = {
  mode: 'development',
  //mode: 'production',

  entry: './src/scripts/index.js',
  output: {
    path: path.resolve(__dirname, 'app/scripts'),
    filename: 'bundle.js',
    publicPath: '/scripts/',
  },

  devtool: 'inline',
  //devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [{
          loader: '@riotjs/webpack-loader',
          options: {
            hot: true, // set it to true if you are using hmr
            // add here all the other @riotjs/compiler options riot.js.org/compiler
            template: 'pug'
          }
        }]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};
```
```javascript:index.js
import {component} from 'riot'
import App from './app.pug'

component(App)(document.getElementById('root'), {
  message: 'Hello World'
});
```
```php:app.pug
my-app
  p { props.message }

  mixin menu(name)
    li
      a(href="/#" + name) #{name}

  ul
    +menu('home')
    +menu('hello')
    +menu('goodbye')
```
```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Riot App</title>
</head>
<body>
  <div id="root"></div>
  <script src="/scripts/bundle.js"></script>
</body>
</html>
```
後は例のごとくお好み。

```json:package.json
{
  "name": "riotv4-pug-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode production --devtool source-map",
    "start": "webpack-dev-server --inline --watch --hot --colors --content-base app/ --open-page index.html --historyApiFallback true -d --port 4500"
  },
  "keywords": [],
  "author": "KAJIKEN <kentaro@kajiken.jp> (http://kajiken.jp)",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.7.2",
    "@babel/preset-env": "^7.7.1",
    "@riotjs/compiler": "^4.5.2",
    "@riotjs/hot-reload": "^4.0.0",
    "@riotjs/webpack-loader": "^4.0.1",
    "babel-loader": "^8.0.6",
    "pug": "^2.0.4",
    "riot": "^4.6.6",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}
```
```json:.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "targets": [
          ">0.25%",
          "not ie 11",
          "not op_mini all"
        ]
      }
    ]
  ]
}
```

ブラウザで見える結果はこのような状態になります。

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Riot App</title>
</head>
<body>
  <div id="root" is="my-app">
    <p>Hello World</p>
    <ul>
      <li><a href="/#home">home</a></li>
      <li><a href="/#hello">hello</a></li>
      <li><a href="/#goodbye">goodbye</a></li>
    </ul>
  </div>
  <script src="/scripts/bundle.js"></script>
</body>
</html>
```

## まとめ
`registerPreprocessor`で登録した内容に従う為、どんなテンプレートエンジンだろうと、テンプレートエンジンですらなかろうと出来る。

`registerPreprocessor`で全ページ共通の処理（ステップ計測とか一律文字置換とか）をさせた上で、いつも通りriotファイルとして扱うことも出来そう。
別の場所からソースを取ってきて置き換えるとかもできると思うので、複数プロジェクトで共通のコンポーネントとかもいける。
夢が広がります。