Viteで純粋なPugを使う
ViteでPugを使おうとすると少し厄介だったので、環境構築の手順をまとめました。
「vite pug」等で検索するとVueやReact、HTMLを噛ませる方法はヒットするのですが、Pugを単体で使う方法はなかなかヒットしません。
Viteで純粋なPugを使えるようにするというのが本記事の趣旨になります。
環境構築
まずは普通に環境構築をします。
Viteプロジェクトの作成
公式ガイドの手順にしたがってViteの環境構築を行います。
本記事ではyarn
を使いますが、npm
でも問題ないと思います。
$ yarn create vite
#...
✔ Project name: … vite-pug-project
✔ Select a framework: › vanilla
✔ Select a variant: › vanilla-ts
#...
プロジェクト名はvite-pug-project
、フレームワークはなし(vanilla
)でTypeScript(vanilla-ts
)を選択しました。
$ cd vite-pug-project # プロジェクトディレクトリに移動
$ yarn # yarn install でモジュールをインストール
$ yarn dev # 開発サーバーの立ち上げ
指示された手順にしたがっているだけですが、これで開発サーバーが立ち上がります。
非常に簡単ですね。
Hello Vite!
Documentation
という文字がブラウザに表示されていれば問題ありません。
環境のカスタマイズ
現場のディクレクトリ構造は以下のようになっていると思います。
vite-pug-project
├── node_modules - ...略
├── favicon.svg
├── index.html
├── package.json
├── src
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── tsconfig.json
└── yarn.lock
このままでもいいのですが、index.html
がルートにあるとMulti-Page Appでは扱いづらいのでsrc
フォルダーの中に移動します(ついでにfaviconもいらないので削除してください)。
それに伴って、Viteの設定ファイルをいじります。
プロジェクトのルートにvite.config.js
を作成して、下記のように記述してください。
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
root: "src",
build: {
outDir: resolve(__dirname, "dist"),
},
});
root(index.html
が置かれる場所)をsrc
に設定して、ビルドするディレクトリをdist
に指定しています。
場所を移動したのでindex.html
も書き換えます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
- <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
- <script type="module" src="/src/main.ts"></script>
+ <script type="module" src="/main.ts"></script>
</body>
</html>
一度yarn dev
で開発サーバー立ち上げてみて、問題なければyarn build
コマンドでビルドしてください。
vite-pug-project
├── node_modules - ...略
├── dist
│ ├── assets
│ │ ├── index.06d14ce2.css
│ │ └── index.ad4f7fa4.js
│ └── index.html
├── package.json
├── src
│ ├── index.html
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── tsconfig.json
├── vite.config.js
└── yarn.lock
無事ビルドができれば上記のようなディレクトリ構造になると思います。
Pugを使えるようにする
ここまできたらPugを使うための準備をします。
まずはPugに必要なモジュールと型定義ファイルをインストールしましょう。
$ yarn add --dev pug @types/pug @types/node
次にindex.html
を削除して、index.pug
を追加しましょう。
以下は単純にindex.html
内容をindex.pug
に書き換えただけのものになります(変化がわかるようにtitle
だけ変更)。
doctype html
html(lang="ja")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
//- index.pugからビルドされたものだとわかるようにtitleだけ変更する
title Vite Pug App
body
#app
script(src="/main.ts" type="module")
ビルド用プラグインの作成
Pugをビルドするためにプラグインを自作します。
プラグインを自作する方法は以下に載っています。
しっかり作ろうとするとrollup.js
の知識も必要となります。
ルートにplugins
ディレクトリを作成して、その中にプラグインを作成しましょう。
import fs from "fs";
import type { Plugin } from "vite";
import { compileFile } from "pug";
export const vitePluginPugBuild = (): Plugin => {
const pathMap: Record<string, string> = {};
return {
// Vite専用プラグインの命名には「vite-plugin-」のプレフィックスをつけるらしい
name: "vite-plugin-pug-build",
enforce: "pre",
// ビルド時のみ
apply: "build",
// カスタムリゾルバーを定義できる
// エントリーポイントの加工をできる
resolveId(source: string) {
if (source.endsWith(".pug")) {
// xxxx.pug へのリクエストを
// xxxx.html へのリクエストに偽る
const dummy = `${
source.slice(0, Math.max(0, source.lastIndexOf("."))) || source
}.html`;
// xxxx.pug と xxxx.html 対応表を作る
pathMap[dummy] = source;
// xxxx.html を返す
return dummy;
}
},
// ローダーを定義できる
// ここでファイルの中身を読み込む
load(id: string) {
if (id.endsWith(".html")) {
// xxxx.html へのリクエストがあった時
if (pathMap[id]) {
// もとのファイルが xxxx.pug の時は pug をコンパイルして返す
const html = compileFile(pathMap[id])();
return html;
}
// もとのファイルも xxxx.html の時は xxxx.html の中身をそのまま返す
return fs.readFileSync(id, "utf-8");
}
},
};
};
上記で何をやっているか簡単に説明すると、xxxx.pug
に来たリクエストをxxxx.html
と偽って(?)、xxxx.pug
をコンパイルした結果を返すということをしています。
resolveId
やload
はrollup.js
に存在するビルドフックで、ビルド時のそれぞれのタイミングで呼び出される関数です。
詳しくは以下のドキュメントをご参照ください。
後ほどもう1つプラグインを作成するので、vite-plugin-pug.ts
を作成しラップしてexport
します。
import { vitePluginPugBuild } from "./vite-plugin-pug-build";
const vitePluginPug = () => {
return [vitePluginPugBuild()];
};
export default vitePluginPug;
プラグインの読み込みとindex.pug
をエントリーポイントとして明示的に指定するためにvite.config.js
を次のように書き換えます。
import { resolve } from "path";
import { defineConfig } from "vite";
import vitePluginPug from "./plugins/vite-plugin-pug";
export default defineConfig({
root: "src",
build: {
outDir: resolve(__dirname, "dist"),
rollupOptions: {
input: {
main: resolve(__dirname, "src", "index.pug"),
},
},
},
plugins: [vitePluginPug()],
});
現状のディクトリ構造は下記のようになります。
vite-pug-project
├── dist
│ ├── assets
│ │ ├── index.06d14ce2.css
│ │ └── index.ad4f7fa4.js
│ └── index.html
├── package.json
├── plugins
│ ├── vite-plugin-pug-build.ts
│ └── vite-plugin-pug.ts
├── src
│ ├── index.pug
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── tsconfig.json
├── vite.config.js
└── yarn.lock
ここまできたら、いったんyarn build
してみます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Vite Pug App なので index.pug がビルドされている -->
<title>Vite Pug App</title>
<script type="module" crossorigin src="/assets/main.94834e27.js"></script>
<link rel="stylesheet" href="/assets/main.b16cdc1a.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
dist
ディレクトリにindex.html
がビルドされていることがわかります!
開発サーバー用のプラグインの作成
ビルドはできましたが、この状態でyarn dev
をして開発サーバーを立ち上げても、index.pug
ファイルを確認することはできません。
この問題を解決するためには開発サーバー用のプラグインも作成する必要があります。
vite-plugin-pug-serve.ts
というファイルを作成して、下記のように記述します。
import fs from "fs";
import { send } from "vite";
import type { ViteDevServer, Plugin } from "vite";
import { compileFile } from "pug";
const transformPugToHtml = (server: ViteDevServer, path: string) => {
try {
const compiled = compileFile(path)();
return server.transformIndexHtml(path, compiled);
} catch (error) {
console.log(error);
return server.transformIndexHtml(path, "Pug Compile Error");
}
};
export const vitePluginPugServe = (): Plugin => {
return {
name: "vite-plugin-pug-serve",
enforce: "pre",
// 開発サーバー時のみ
apply: "serve",
handleHotUpdate(context) {
// ファイルが保存された時にホットリロードする
// この記述がないと xxxx.pug を保存した時にリロードされない
context.server.ws.send({
type: "full-reload",
});
return [];
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const root = server.config.root;
let fullReqPath = root + req.url;
if(fullReqPath.endsWith("/")){
fullReqPath += "index.html"
}
if (fullReqPath.endsWith(".html")) {
// xxxx.html にリクエストがきた時
if (fs.existsSync(fullReqPath)) {
// xxxx.html が存在するならそのまま次の処理へ
return next();
}
// xxxx.htmlが存在しないときは xxxx.pug があるか確認する
const pugPath = `${
fullReqPath.slice(0, Math.max(0, fullReqPath.lastIndexOf("."))) ||
fullReqPath
}.pug`;
if(!fs.existsSync(pugPath)){
// xxxx.pug が存在しないなら 404 を返す
return send(req, res, "404 Not Found", "html", {});
}
// xxxx.pug が存在するときは xxxx.pug をコンパイルした結果を返す
const html = await transformPugToHtml(server, pugPath);
return send(req, res, html, "html", {});
} else {
// xxxx.html 以外へのリクエストはそのまま次の処理へ
return next();
}
});
},
};
};
上記では開発サーバーを立ち上げた状態でxxxx.html
へのリクエストがきたらxxxx.pug
をコンパイルして結果を返すという処理を記述しています(xxxx.html
ファイルが存在するときはそれをそのまま返す)。
send()
メソッドについては公式ドキュメントに記載があったわけではないのですが、他の方のプラグインで使っていたので存在を知りました。
上記を見る限りだと、拡張子の種類によって適切なヘッダーを付与してレスポンスを返してくれるメソッドのようです。
最後にvite-plugin-pug-build.ts
と同様にvite-plugin-pug.ts
からまとめてexport
します。
import { vitePluginPugBuild } from "./vite-plugin-pug-build";
import { vitePluginPugServe } from "./vite-plugin-pug-serve";
const vitePluginPug = () => {
return [vitePluginPugBuild(), vitePluginPugServe()];
};
export default vitePluginPug;
ここまで書いたらyarn dev
して開発サーバーを立ち上げてください。
Hello Vite!
Documentation
が表示されていれば完璧です。
Pugコンパイル時のオプションと変数の設定
せっかくなのでvite.config.js
からPugコンパイル時のオプションと変数を設定できるようにします。
ここは必要なければ(もしくはvite-plugin-pug-**.ts
に直接書くのであれば)飛ばしても問題ありません。
本記事ではpug.compileFile
を使用しているので、詳しくは以下のドキュメントを参照してください。
// pug.compileFile
// 下記の options と locals を vite.config.js から渡せるようにする
var pug = require('pug');
// Compile a function
var fn = pug.compileFile('path to pug file', options);
// Render the function
var html = fn(locals);
// => '<string>of pug</string>'
さてドキュメントを確認したところで、以下のようにvite.config.js
からoptions
とlocals
を渡せるようにします。
本記事ではbuild
とserve
で別々に設定できるようにしていますが、プロジェクトによって同一で問題なければ、適宜修正してください。
import { resolve } from "path";
import { defineConfig } from "vite";
import vitePluginPug from "./plugins/vite-plugin-pug";
export default defineConfig({
root: "src",
build: {
outDir: resolve(__dirname, "dist"),
rollupOptions: {
input: {
main: resolve(__dirname, "src", "index.pug"),
},
},
},
plugins: [
// options と locals の設定例
vitePluginPug({
+ build: {
+ locals: { hoge: "hoge" },
+ options: { pretty: true },
+ },
+ serve: {
+ locals: { hoge: "hoge" },
+ options: { pretty: true },
+ },
}),
],
});
まずvite-puglin-pug.ts
を以下のように修正します。
やっていることは単純でvite.config.js
から渡された引数を、それぞれvite-plugin-pug-build.ts
とvite-plugin-pug-serve.ts
に渡している形になります。
import type { LocalsObject, Options } from "pug";
import { vitePluginPugBuild } from "./vite-plugin-pug-build";
import { vitePluginPugServe } from "./vite-plugin-pug-serve";
// 引数の型定義
type PugSettings = {
options: Options;
locals: LocalsObject;
};
// オプショナルな引数として、options と locals を受け取る
const vitePluginPug = (settings?: {
build?: Partial<PugSettings>;
serve?: Partial<PugSettings>;
}) => {
// build用の options と locals
const buildSettings = {
options: { ...settings?.build?.options },
locals: { ...settings?.build?.locals },
};
// serve用の options と locals
const serveSettings = {
options: { ...settings?.serve?.options },
locals: { ...settings?.serve?.locals },
};
// それぞれ引数として渡す
return [
vitePluginPugBuild({
options: buildSettings.options,
locals: buildSettings.locals,
}),
vitePluginPugServe({
options: serveSettings.options,
locals: serveSettings.locals,
}),
];
};
export default vitePluginPug;
次にvite-plugin-pug-build.ts
を修正します。
受け取ったoptions
とlocals
をcompileFile
にセットしています。
import fs from "fs";
import type { Plugin } from "vite";
import { compileFile } from "pug";
import type { LocalsObject, Options } from "pug";
// 引数の型定義
type PugSettings = {
options: Options;
locals: LocalsObject;
};
// options と locals を引数として受け取る
export const vitePluginPugBuild = ({options, locals}: PugSettings): Plugin => {
const pathMap: Record<string, string> = {};
return {
name: "vite-plugin-pug-build",
enforce: "pre",
apply: "build",
resolveId(source: string) {
if (source.endsWith(".pug")) {
const dummy = `${
source.slice(0, Math.max(0, source.lastIndexOf("."))) || source
}.html`;
pathMap[dummy] = source;
return dummy;
}
},
load(id: string) {
if (id.endsWith(".html")) {
if (pathMap[id]) {
// options locals を compileFile にセットする
const html = compileFile(pathMap[id], options)(locals);
return html;
}
return fs.readFileSync(id, "utf-8");
}
},
};
};
最後にvite-plugin-pug-serve.ts
を修正します。
build時と同様にoptions
とlocals
をcompileFile
にセットしています。
import fs from "fs";
import { send } from "vite";
import type { ViteDevServer, Plugin } from "vite";
import { compileFile } from "pug";
import type { LocalsObject, Options } from "pug";
// 引数の型定義
type PugSettings = {
options: Options;
locals: LocalsObject;
};
const transformPugToHtml = (
server: ViteDevServer,
path: string,
options: Options,
locals: LocalsObject
) => {
try {
// options と locals を compileFile にセットする
const compiled = compileFile(path, options)(locals);
return server.transformIndexHtml(path, compiled);
} catch (error) {
console.log(error);
return server.transformIndexHtml(path, "Pug Compile Error");
}
};
// options と locals を引数として受け取る
export const vitePluginPugServe = ({ options, locals }: PugSettings): Plugin => {
return {
name: "vite-plugin-pug-serve",
enforce: "pre",
apply: "serve",
handleHotUpdate(context) {
context.server.ws.send({
type: "full-reload",
});
return [];
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const root = server.config.root;
let fullReqPath = root + req.url;
if (fullReqPath.endsWith("/")) {
fullReqPath += "index.html";
}
if (fullReqPath.endsWith(".html")) {
if (fs.existsSync(fullReqPath)) {
return next();
}
const pugPath = `${
fullReqPath.slice(0, Math.max(0, fullReqPath.lastIndexOf("."))) ||
fullReqPath
}.pug`;
if (!fs.existsSync(pugPath)) {
return send(req, res, "404 Not Found", "html", {});
}
// options と locals も渡す
const html = await transformPugToHtml(server, pugPath, options, locals);
return send(req, res, html, "html", {});
} else {
return next();
}
});
},
};
};
お疲れ様でした。以上で完成となります。
実際にoptions
やlocals
を設定して、ビルドもしくは開発サーバーを立ち上げてみると、設定が反映されていることがわかると思います。
ボイラープレート
ここまでの作業をボイラープレートとしてまとめてあります。
全体の完成コードを確認したい方は上記をご参照ください。
パフォーマンスについて
本記事の内容は必ずしもViteに最適化された(想定された)方法ではない可能性があります。
いったん細かなパフォーマンスのことは置いておいて、従来の方法でPugを使えるようにするというのが趣旨になります。
(!) Could not auto-determine entry point from rollupOptions or html files and there are no explicit optimizeDeps.include patterns. Skipping dependency pre-bundling.
たとえば本記事の内容で開発サーバーを立ち上げるとターミナルに上記のような文言が表示されます。
vite.config.js
で指定したエントリーポイントがxxxx.pug
なので、(おそらく想定している拡張子とは違って?)うまくパスの解決ができていないようです。
それによりdependency pre-bundling
がされないようなのですが、パフォーマンスへの影響がどのくらいあるのかは未検証です(というか詳しい方いたら教えたください)。
dependency pre-bundling
については以下に載っています。
まとめ
ViteでPug単体を使う方法でした。正攻法ではないかもしれませんが、webpackや他のビルドツールに変わるオプションの1つとして備えておくのは十分ありだと思います。
今回はじめてVite(またrollup.js)を触ったのですが、自作プラグインの作り方やビルドフックについて理解が進んだので、違う機会でも活かせそうだなと感じました。ぜひ皆さんもいろいろ触ってみてください。
vite-plugin-pug-staticについて
【追記 - 2022.07.17】
現在、Pugを静的HTMLとして配信・出力するプラグイン、vite-plugin-pug-static
が開発されています。
実は本記事を書くにあたって、誰か良い感じにプラグインを作ってくれ(他人任せ)という気持ちがあったので、大変ありがたく思います。
詳しくは以下の記事をご参照ください。
参考
Discussion
こんにちは、参考にさせていただきました。とてもわかりやすく実行できました。
一点ご質問なのですが、pug構文でのエラーの際にnpm run devが落ちてしまうのですが、こちらは参考にさせて頂いたコード上そうなってしまうのでしょうか?もしくはviteがそれに対応していないのでしょうか?
もし御時間ありましたらご回答いただけると助かります。
tamon.kondo さん、ご指摘ありがとうございます。
これは私の参考コードが良くなかったですね。失礼しました。
plugins/vite-plugin-pug-serve.ts
のtransformPugToHtml
に対して上記のように例外処理を挟んでいただくと、Pugのコンパイルエラー時でも開発サーバーが落ちなくなると思います。本記事内の方も修正させていただきました。
ありがとうございます!
無事動作致しました。
静的ページの複数対応とかだと下記リンクとの併用できてすごく助かりました。