WordPress + Bootstrap + Vite

Bootstrap を使った WordPress のテーマ制作で、デファクトな開発フローはあるのだろうか。とりあえず Bootstrap 全部入りの CSS と JS を WP のテンプレートで読み込んで、ある程度形になった時点で Bootstrap の必要なコンポーネントをビルド、テンプレートの CSS と JS を差し替えて確認という流れで始めたが、ちょっとめんどくさいのでいくつかの自動化コードを書いた。

大まかな流れは変わらないが、WPのテーマではCSSは触らずテンプレートの編集のみに、BSはVite環境で編集してCSSとJSのバンドルを作成。WPのサーバ(Apache)とBSのサーバ(Node)間のファイルのやりとりを自動化する。

SCSS編集のためのHTMLファイルの取得はVite環境立ち上げ時にGETで取得し、HMR (Hot Module Replacement)のためにタグを差し込む。この流れはPythonでおこない、npm startスクリプト("start": "python actions/_get_html.py; vite")に追加。

_get_html.py
import concurrent.futures as F
import requests, pathlib, io

FILES = {
    'index.html': 'http://localhost/wordpress/',
    'hello-world.html': 'http://localhost/wordpress/index.php/2022/11/11/hello-world/',
    'test.html': 'http://localhost/wordpress/index.php/2022/11/24/test/',
    'archive.html': 'http://localhost/wordpress/index.php/2018/11/page/2/',
    'search.html': 'http://localhost/wordpress/?s=help',
    '404.html': 'http://localhost/wordpress/wow',
}

def replace_script_tags(name, url):
    html = requests.get(url)
    path = pathlib.Path(name)
    proj_dir = pathlib.Path(__file__).parent.parent
    dest = proj_dir / 'src'
    name_raw = proj_dir / f'supports/{path.stem}.raw{path.suffix}'

    with open(name_raw, 'wb') as raw,          open(dest / name, 'w') as dst:
        raw.write(html.content)
        src = io.StringIO(html.text)
        i = target_linenum = 0
        lines = []
        while line := src.readline():
            if not line.startswith('<!-- tinyc-scripts -->'):
                lines.append(line)
                i += 1
            else:
                target_linenum = i
        for i, line in enumerate(lines):
            if i == target_linenum:
                dst.write('<script type="module" src="./js/main.js"></script>
')
            dst.write(line)
    return f'{url} => {name}'

if __name__ == '__main__':
    with F.ThreadPoolExecutor() as exctr:
        fs = {exctr.submit(replace_script_tags, name, FILES[name]) for name in FILES}
        for f in F.as_completed(fs):
            print(f.result())

HMRのタグを挿入するためのマーカー(<!-- tinyc-scripts -->)はWPテーマのfunctions.phpで、style_loader_tagscript_loader_tagのフックにフィルターを追加。

from functions.php
function tinyc_add_scripts_marker($tag, $handle) {
    if (preg_match('/^tinyc-(?:script|style)-/', $handle)) {
        $tag = '<!-- tinyc-scripts -->' . rtrim($tag) . "<!-- /tinyc-scripts -->
";
    }
    return $tag;
}
add_filter('style_loader_tag', 'tinyc_add_scripts_marker', 10, 2);
add_filter('script_loader_tag', 'tinyc_add_scripts_marker', 10, 2);

スタイル編集後のバンドルファイルは、Vite Plugin APIのcloseBundleにフックするコードをvite.config.jsに追加。Viteのdev環境では編集してもバンドルファイルが作成されるわけではないので、kill -TERMした時だけcloseBundle時にnpm run buildして生成されたファイルをコピーするようにした。

vite.config.js
import { spawn } from 'node:child_process';
const path = require('path');

const docroot = path.resolve(__dirname, 'src');
const wpAssetsDir = '/Users/osmatsuda/Works/web/htdocs/www/wordpress/wp-content/themes/tinyc/assets';
const postAction = {
    name: 'my-post-action',
    apply: 'serve',
    closeBundle() {
        const action = spawn('npm', ['run', 'build']);
        const reAssets = /\b(dist\/assets\/([^/]+)(\.css|\.js))\b/
        return new Promise((resolve, reject) => {
            action.stdout.on('data', (data) => {
                message = ''
                if (data instanceof ArrayBuffer) {
                    message = new TextDecoder().decode(new Uint8Array(data));
                } else {
                    message = data.toString();
                }
                console.log(message.trimEnd());

                const m = message.match(reAssets);
                if (m) {
                    const from = path.resolve(docroot, m[1]);
                    const to = path.resolve(wpAssetsDir, m[2]+m[3]);
                    spawnSync('python', ['actions/_cleanup.py', to]);
                    spawnSync('cp', [from, to]);
                    spawnSync('python', ['actions/_diff.py']);
                }
            });
            action.on('exit', (code) => {
                resolve();
            });
        });
    }
};

export default {
    root: docroot,
    resolve: {
        alias: {'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap')}
    },
    server: {
        host: true,
        port: 8080,
        hot: true,
    },
    plugins: [postAction],
}

あと、Vite環境上でもHTMLの編集が発生するかもしれないので、上記のフック内からDiff作成のためのコードも呼んでいる。

_diff.py
import difflib, pathlib, sys, io
import _get_html

def make_diff(from_file, to_file, diff_file):
    with open(from_file) as ff,          open(to_file) as tf,          open(diff_file, 'w') as out:

        differ = difflib.HtmlDiff()
        deltas = differ.make_file(
            ff.readlines(),
            tf.readlines(),
            fromdesc=from_file.name,
            todesc=to_file.name,
            context=True,
            numlines=3,
        )
        deltas = io.StringIO(deltas).readlines()
        for i, d in enumerate(deltas):
            if d.strip() == '</style>':
                break
        deltas.insert(i, '        td[nowrap] {width:45vw; display:block; overflow:scroll}
');
        out.writelines(deltas)

def _name_variations(fname):
    name = pathlib.Path(fname)
    return f'{name.stem}.raw{name.suffix}', f'{name.stem}.diff{name.suffix}'

def main(fname):
    proj_dir = pathlib.Path(__file__).parent.parent
    from_file_name, diff_file_name = _name_variations(fname)
    from_file = proj_dir / f'supports/{from_file_name}'
    diff_file = proj_dir / f'supports/{diff_file_name}'
    to_file = proj_dir / f'src/{fname}'
    make_diff(from_file, to_file, diff_file)

if __name__ == '__main__':
    for fname in _get_html.FILES:
        main(fname)

Vite(Rollup.js)はこのプロジェクトで初めて使ったのでまだ全貌を把握できていないけど既に気に入っている。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です