Vinyl

段階的にTailwindCSSを導入する用のライブラリを作ってみた

はじめに

技術記事のトレンドを見ていてもTailwindCSSは非常に人気で、現状別のCSSライブラリを使っているが切り替えたいという要望は今後多少はありそうだと感じたので、自分ならどう対応するかというモチベーションで処理を作り、ついでにライブラリにしてみました。

GitHub - mdkk11/tw-preflight-scope: A Tailwind CSS plugin for scoped preflight stylesA Tailwind CSS plugin for scoped preflight styles. Contribute to mdkk11/tw-preflight-scope development by creating an account on GitHub.favicon icongithub.com

詳細をダラダラと解説

内容的には指定した範囲内にPreflightCSSを適用するだけなので大したコード量もありません。

以下ダラダラと解説します。

import * as fs from 'fs';
import * as path from 'path';
import postcss from 'postcss';
import { withOptions } from 'tailwindcss/plugin.js';
import { CSSRuleObject } from 'tailwindcss/types/config';
 
interface PluginOptions {
  scope: string;
}
 
const preflightCssPath = path.resolve(require.resolve('tailwindcss'), '../css/preflight.css');
 
function convertToCSSRuleObject(rule: postcss.Rule): CSSRuleObject {
  const declarations: Record<string, string> = {};
  rule.nodes.forEach((node) => {
    if (node.type === 'decl') {
      declarations[node.prop] = node.value;
    }
  });
  return { [rule.selector]: declarations };
}
 
export const twPreflightScope = withOptions<PluginOptions>(
  (options) =>
    ({ addBase }) => {
      const preflightCss = fs.readFileSync(preflightCssPath, 'utf-8');
      const root = postcss.parse(preflightCss);
      const cssRules: CSSRuleObject[] = [];
      root.walkRules((rule) => {
        rule.selectors = rule.selectors.map((s) => `${s}:where(${options.scope}, ${options.scope} *)`);
        rule.selector = rule.selectors.join(',\n');
        if (rule.nodes.length > 0) {
          cssRules.push(convertToCSSRuleObject(rule));
        }
      });
      addBase(cssRules);
    },
  () => ({
    corePlugins: {
      preflight: false,
    },
  })
);

まず、指定した範囲だけ適用したいので、preflightCSSを全体の設定では無効にしています。

() => ({
    corePlugins: {
      preflight: false,
    },
  })

そしてwithOptionsというTailwindCSSのプラグイン作成関数を使い、その中でベーススタイルを追加するためにaddBaseというプラグインAPIの関数を設定しています。

Adding custom styles - Core conceptsBest practices for adding your own custom styles in Tailwind projects.favicon icontailwindcss.com

interface RecursiveKeyValuePair<K extends keyof any = string, V = string> {
  [key: string]: V | RecursiveKeyValuePair<K, V>
}
type CSSRuleObject = RecursiveKeyValuePair<string, null | string | string[]>
 
addBase(base: CSSRuleObject | CSSRuleObject[]): void

型情報から分かるように、addBaseという関数は以下のような形式のオブジェクトを引数にとります。

const exampleRule: CSSRuleObject = {
  ".exampleStyle": { color: "red", fontSize: "16px", "&:hover": { color: "blue" } },
};

なので取り込んだTailwindPreflightCSSのままでは適用がでいない為、convertToCSSRuleObject関数で指定されているCSSRuleObjectの形式に変換をしています。

function convertToCSSRuleObject(rule: postcss.Rule): CSSRuleObject {
  const declarations: Record<string, string> = {};
  rule.nodes.forEach((node) => {
    if (node.type === 'decl') {
      declarations[node.prop] = node.value;
    }
  });
  return { [rule.selector]: declarations };
}

下記のようにparseされたCSSオブジェクトにてPostCSSのAPIを使い各ルールに対して反復処理を行い、各セレクタに:where()疑似クラスを使用してスコープを適用し、指定された範囲のみPreflightが適用されるようになっています。

PostCSS APITransform CSS with the power of JavaScript. Auto-prefixing, future CSS syntaxes, modules, linting and more are possible with hundreds of PostCSS plugins.favicon iconpostcss.org

const preflightCss = fs.readFileSync(preflightCssPath, 'utf-8');
const root = postcss.parse(preflightCss);
const cssRules: CSSRuleObject[] = [];
root.walkRules((rule) => {
rule.selectors = rule.selectors.map((s) => `${s}:where(${options.scope}, ${options.scope} *)`);
rule.selector = rule.selectors.join(',\n');
if (rule.nodes.length > 0) {
  cssRules.push(convertToCSSRuleObject(rule));
  }
 });
addBase(cssRules);

使い方

設定はTailwindCSSの設定ファイルであるtailwind.config.jspluginsに追加するだけです。その際に引数で、スコープに使用したいクラス名を指定する必要があります。

const { twPreflightScope } = require('tw-preflight-scope');
 
module.exports = {
  // ...
  plugins: [
    twPreflightScope({ scope: '.tw-scope' }),
    // ...
  ],
};

あとはいつも通りのTailwindCSSの設定をし、適用したい箇所を先ほど指定したクラス名で囲むだけです。

実際に実務で使うのであれば自分はこんな感じのラッパーを作って対応すると思います。

export const TailwindScopeWrapper = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  return <div className="tw-scope">{children}</div>;
};

さいごに

正直個人的にライブラリを作ってみたいというモチベーションがあったからわざわざライブラリ化しただけなので、実際は処理をコピペして使うだけでいいと思います。(多分自分も次の現場でもそうすると思います)