mizzsugar’s blog

日々感じていることや学んだことを書きます。エンジニアリング以外にも書くかもしれません。

html/CSSをESlintのルールに寄せてCI走らせた

背景

今作っているWEBサービスでは UI/UX(look and feelと言われるところ)を担当するデザイナーさんが、look and feelのデザインを考えHTML/CSSに落とし込みます。

JavaScriptで実際に動きをつける部分は別の人が担当します。※1

我々は、Nuxt.jsを使っているのでHTML/CSSファイルをvueファイルに置き換える必要があります。

この時、html/cssほぼコピペでvueに置き換えるので済むと楽です。

ただ、そうはいかない場合があります。

Nuxt.jsの部分はESlintを採用しており、ESlintを走らせるとhtml/cssの内容をコピペのままだと落ちる場合があります。

そこで、HTML/CSSにもLintを実行することにしました。


Linterは何にする?

htmlとCSSのLinterとして、htmllintcsslintがありました。

それらにしようと思いましたが辞めました。

ESlintのルールと同じではなく、ESlintに寄せるのが大変であるためです。

js-beautifyも考えましたが、あくまで整形するだけで注意はしてくれないのでCI向きではないなと諦めました。

ESlintと近いルールのLinterを探しましたが見つかりませんでした。

ESlintをhtml, cssファイルに走らせてみましたが、出来なかったので力技感はありますがhtmlをvueに置き換えてESlintを走らせることにしました。これだけいうと何言ってるか分かりませんので説明します↓


htmlからvueへ

CIで実現したいことを図にまとめるとこんな感じです。

f:id:mizzsugar:20200425161319p:plain

拡張子が.html, .cssのファイルの中身をコピーした.vueファイルを生成します。

といってもそのまま、.vueにするとsintax errorになるのでvueに合わせます。なお、vueファイルはCIを実行する際に生成されるもので、リポジトリには残りません。

生成はPythonスクリプトで実行します。Pythonの方が得意のなのでPythonにしていますが、nodeでも出来ると思います。


例↓

index.html (元)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bulma@0.8.1/css/bulma.min.css" rel="stylesheet" type="text/css">
    <link href="css/style.css" rel="stylesheet" type="text/css">
    <title>タイトル</title>
  </head>
  <body>
    <p>aaaaaaaaaaaaaaaaaaaa</p>
  </body>
</html>

index.vue (htmlを変換後)

<template>
  <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link href="https://cdn.jsdelivr.net/npm/bulma@0.8.1/css/bulma.min.css" rel="stylesheet" type="text/css">
        <link href="css/style.css" rel="stylesheet" type="text/css">
        <title>タイトル</title>
    </head>
    <body>
        <p>aaaaaaaaaaaaaaaaaaaa</p>
    </body>
  </html>
</template>


style.css(元)

@charset "UTF-8";
/* CSS Document */

html{
  font-family: "Noto Sans Regular", "open-sans", YuGothic, "Yu Gothic medium", "Hiragino Sans", Meiryo, "sans-serif";
}

body{
  margin: 0;
  padding: 0;
}

style.vue (cssを変換後)

<template>
  <div />
</template>
<style>
@charset "UTF-8";
/* CSS Document */

html{
  font-family: "Noto Sans Regular", "open-sans", YuGothic, "Yu Gothic medium", "Hiragino Sans", Meiryo, "sans-serif";
}

body{
  margin: 0;
  padding: 0;
}
</style>


lint.py

import os
import pathlib


def _html_to_vue(target_file: pathlib.PosixPath) -> None:
    written = pathlib.Path(f'./_dest/{target_file}.vue')
    written.parent.mkdir(parents=True, exist_ok=True)
    with target_file.open() as r:
        with written.open(mode='w') as w:
            w.write('<template>\n')
            for line in r.readlines():
                if '<!doctype html>' in line:
                    continue
                if line == '\n':
                    w.write(line)
                    continue
                w.write(f'  {line}')
            w.write('</template>\n')  


def _css_to_vue(target_file: pathlib.PosixPath) -> None:
    written = pathlib.Path(f'./_dest/{target_file}.vue')
    written.parent.mkdir(parents=True, exist_ok=True)
    with target_file.open() as r:
        with written.open(mode='w') as w:
            w.write(
                '<template>\n'
                '  <div />\n'
                '</template>\n'
                '<style>\n'
            )
            for line in r.readlines():
                w.write(line)
            w.write('</style>\n')


def main() -> None:
    for html_file in pathlib.Path('.').glob('**/*.html'):
        _html_to_vue(html_file)

    for css_file in pathlib.Path('.').glob('**/*.css'):
        _css_to_vue(css_file)


if __name__ == '__main__':
    main()


次にCIを組みます。

Github Actionsを使います。

CIで起こることの手順はこうです↓

  1. Pythonスクリプトでhtml/cssをvueに置き換える

  2. 1で生成されたvueファイルに対してESlintが実行される

.github/workflows/main.yml

name: CI

on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: actions/cache@v1
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - run: python3 lint.py
      - run: npm set progress=false && npm ci
      - run: npm run lint


これでPRが提出されるたびにCIが走ってESlintのルールでhtml/cssにlintが実行されます。

じゃっかんhtml/cssファイルの行とずれていますが、nuxtで実装するときのESlintのルールがそのまま適用されるので置き換えが楽です。

サンプルリポジトリ

github.com

※1 この体制が良いか悪いかはこの記事の観点ではないのでご意見を受け付けません。ご了承ください。