段階的にTailwindCSSを導入する用のライブラリを作ってみた
はじめに
技術記事のトレンドを見ていてもTailwindCSS
は非常に人気で、現状別のCSSライブラリを使っているが切り替えたいという要望は今後多少はありそうだと感じたので、自分ならどう対応するかというモチベーションで処理を作り、ついでにライブラリにしてみました。
詳細をダラダラと解説
内容的には指定した範囲内に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の関数を設定しています。
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" } },
};
なので取り込んだTailwind
のPreflightCSS
のままでは適用がでいない為、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
が適用されるようになっています。
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.js
のplugins
に追加するだけです。その際に引数で、スコープに使用したいクラス名を指定する必要があります。
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>;
};
さいごに
正直個人的にライブラリを作ってみたいというモチベーションがあったからわざわざライブラリ化しただけなので、実際は処理をコピペして使うだけでいいと思います。(多分自分も次の現場でもそうすると思います)