Open1

PostCSSのプラグイン書くやつの翻訳メモ

AkatsukiAkatsuki

Writing a PostCSS Plugin

https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md を自分用にてきとーに日本語に訳したやつ

リンク集

ドキュメント:

サポートページ:

Step 1: Create an idea

新しいPostCSSプラグインを自分で書くことで、あなたの役に立つことはたくさんあります。

  • 互換性: ブラウザの互換性のためのコードを追加するのをいつも忘れてしまう場合は、そのためのコードを自動的に挿入してくれるPostCSSプラグインを作ることができます。postcss-flexbugs-fixespostcss-100vh-fixがそのいい例です。
  • ルーチンワークの自動化: ルーチンワークをコンピュータに任せることで クリエイティブな作業に専念できます。例えば、RTLCSSを使ったPostCSSではデザインを右から左への言語(アラビア語やヘブライ語など)に自動的に変換できます。また、postcss-dark-theme-classを使用すると、ダーク/ライトテーマのスイッチにメディアクエリを挿入することができます。
  • ありがちな間違いを防止: "エラーが2回起きたなら、また起きるだろう"というフレーズがあります。PostCSSプラグインはソースコードにありがちな間違いをチェックして、不必要なデバッグのための時間を節約することができます。そのための最良の方法は、新しいStylelintプラグインを書くことです。Stylelintは内部でPostCSSを使用しています
  • 保守性の向上: CSSモジュールpostcss-autoresetは、PostCSSが分離することでコードの保守性を高めることができる素晴らしい例です。
  • ポリフィル: postcss-preset-envでは、すでにCSSドラフト用のポリフィルがたくさん用意されています。もし新しいドラフトを見つけたら、新しいプラグインを追加して、このプリセットに送ることができます。
  • 新しいCSSのシンタックス: 私たちは、CSSに新しい構文を追加することをお勧めしません。新しい機能を追加したいのであれば、CSSドラフト提案を書いてCSSWGに送り、それからポリフィルを実装するのが常に良いでしょう。しかし、プロポーザルを送ることができない場合もたくさんあります。例えば、ブラウザのパーサの性能によってCSSWGのnestedの構文が制限されていて、postcss-nested.htmlから非公式のSassのような構文を使いたい場合があります。

Step 2: プロジェクトの作成

プラグインの書き方には2種類あります。

  • プライベートなプラグインを作成します。この方法は、プラグインがプロジェクトの特定のものに関連している場合にのみ使用します。例えば、独自のUIライブラリの特定のタスクを自動化したい場合などです。
  • パブリックなプラグインを公開します。これは常に推奨される方法です。プライベートなフロントエンドシステムは、たとえGoogleであっても、しばしばメンテナンスされなくなることを覚えておいてください。一方で、人気のあるプラグインの多くは、クローズドなプロジェクトでの作業中に作られたものです。

プライベートなプラグインの場合は、

  1. postcss/フォルダの中にプラグインと同名のファイルを作成
  2. ボイラープレートからplugin templateをコピー

パブリックなプラグインの場合は

  1. PostCSS plugin boilerplate のガイドにしたがってプラグインを作成するためのディレクトリを作る
  2. GithubがGitLab上にレポジトリを作成
  3. そこにコードを公開

また、私たちのSharecコンフィグを使って、ベストプラクティスを最新に保つこともできます。この設定を更新するたびに、開発用の設定や開発ツールも更新されます。

module.exports = (opts = {}) => {
  // Plugin creator to check options or prepare caches
  return {
    postcssPlugin: 'PLUGIN NAME'
    // Plugin listeners
  }
}
module.exports.postcss = true

Step 3: Find nodes

ほとんどのPostCSSプラグインは次の2つのことを行います。

  1. CSSのなかから何かを探す (例: will-changeプロパティ)
  2. 探し出した要素に変更を加える (例: 古いブラウザ向けのポリフィルとしてwill-changeの前にtransform: translateZ(0)を挿入する)

PostCSSは、CSSを解析してノードのツリー(私たちはこれをASTと呼んでいます)を作ります。このツリーには次のコンテンツがあるかもしれません。

  • Root: CSSファイルを表す、ツリーのルートノード
  • AtRule: @charset "UTF-8"@media (screen) {}のように@で始まる文
  • Rule: 中にブロックのあるセレクタ 例:input, button {}
  • Declaration: color: black;のようなキーペア
  • Comment: 独立したコメントです。セレクタ内のコメント、アトリビュート、パラメータや値は、ノードの raws プロパティに格納されます。

AST Explorerを使えば、PostCSSがCSSをどのようにASTに変換するか学べます。

プラグインオブジェクトにメソッドを追加することで、特定のタイプのノードをすべて見つけることができます。

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'PLUGIN NAME',
    Once (root) {
      // すべてのファイルが単一のRootを持っているので、ファイルごとに一度だけ呼び出されます。
    },
    Declaration (decl) {
      // すべてのDeclarationに対して呼び出されます
    }
  }
}
module.exports.postcss = true

plugin’s eventsにプラグインイベントの一覧が載っています。

If you need declaration or at-rule with specific names, you can use quick search:

特定の名前の宣言やatルールが必要な場合は、クイックサーチをご利用ください。

    Declaration: {
      color: decl => {
        // All `color` declarations
      }
      '*': decl => {
        // All declarations
      }
    },
    AtRule: {
      media: atRule => {
        // All @media at-rules
      }
    }

その他のケースでは、正規表現や特定のパーサーを使用することができます。

ASTを解析する他のツール:

正規表現やパーサは重い処理であることを忘れてはいけません。処理の重いツールでノードをチェックする前に、String#includes()でクイックテストを行うことができます。

if (decl.value.includes('gradient(')) {
  let value = valueParser(decl.value)}

There two types or listeners: enter and exit.

Once, Root, AtRule, and Rule will be called before processing children.

OnceExit, RootExit, AtRuleExit, and RuleExit after processing all children inside node.

リスナー間でデータを再利用したい場合があります。ランタイム定義のリスナーを使えば可能です:

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'vars-collector',
    prepare (result) {
      const variables = {}
      return {
        Declaration (node) {
          if (node.variable) {
            variables[node.prop] = node.value
          }
        },
        OnceExit () {
          console.log(variables)
        }
      }
    }
  }
}

prepare()を使ってリスナーを動的に生成することができます。例えば、Browserslistを使って宣言のプロパティを取得する場合などです。

Step 4: ノードの変更

目当てのノードを見つけたら、そのノードを変更したり、周囲の他のノードを挿入/削除する必要があります。

PostCSSのnodeはASTを変換するDOMライクなAPIを持っています。API docsをご覧ください。

ノードは、(Node#nextNode#parentのように)移動したり、(Container#someのように)子を探したり、ノードを削除したり、内部に新しいノードを追加したりするメソッドを持っています。

プラグインのメソッドは、第2引数にノードクリエーターを受け取ります。

    Declaration (node, { Rule }) {
      let newRule = new Rule({ selector: 'a', source: node.source })
      node.root().append(newRule)
      newRule.append(node)
    }

新しいノードを追加した場合は、正しいソースマップを生成するために、Node#sourceをコピーすることが重要です。

プラグインは、あなたが変更または追加したすべてのノードを再訪問します。子ノードを変更した場合は、親ノードも同様に再訪問します。ただし、OnceOnceExitだけは再呼び出しされません。

const plugin = () => {
  return {
    postcssPlugin: 'to-red',
    Rule (rule) {
      console.log(rule.toString())
    },
    Declaration (decl) {
      console.log(decl.toString())
      decl.value = 'red'
    }
  }
}
plugin.postcss = true

await postcss([plugin]).process('a { color: black }', { from })
// => a { color: black }
// => color: black
// => a { color: red }
// => color: red

ビジターは何か変更があるとノードに再訪問するため、単にchildrenを追加するだけでは無限ループに陥ります。これを防ぐためには、このノードがすでに処理されているかどうかを確認する必要があります。

    Declaration: {
      'will-change': decl => {
        if (decl.parent.some(decl => decl.prop === 'transform')) {
          decl.cloneBefore({ prop: 'transform', value: 'translate3d(0, 0, 0)' })
        }
      }
    }

Symbolを使って処理済みのノードに印をつけることもできます。

const processed = Symbol('processed')

const plugin = () => {
  return {
    postcssPlugin: 'example',
    Rule (rule) {
      if (!rule[processed]) {
        process(rule)
        rule[processed] = true
      }
    }
  }
}
plugin.postcss = true

2番目の引数には、warningを追加するための result オブジェクトもあります。

    Declaration: {
      bad: (decl, { result }) {
        decl.warn(result, 'Deprecated property bad')
      }
    }

プラグインが他のファイルに依存している場合は、resultにメッセージを添付して、このファイルが変更されたときにCSSを再構築すべきであることをランナー(webpack、Gulpなど)に知らせることができます。

    AtRule: {
      import: (atRule, { result }) {
        const importedFile = parseImport(atRule)
        result.messages.push({
          type: 'dependency',
          plugin: 'postcss-import',
          file: importedFile,
          parent: result.opts.from
        })
      }
    }

もしディレクトリに依存している場合は、メッセージタイプにdir-dependencyを使用してください。

result.messages.push({
  type: 'dir-dependency',
  plugin: 'postcss-import',
  dir: importedDir,
  parent: result.opts.from
})

構文エラー(カスタムプロパティの未定義など)が見つかった場合は、特別なエラーを投げることができます。

if (!variables[name]) {
  throw decl.error(`Unknown variable ${name}`, { word: name })
}

Step 5: Fight with frustration

I hate programming

I hate programming

I hate programming

It works!

I love programming

簡単なプラグインであっても、バグが発生し、デバッグに最低10分はかかるでしょう。シンプルなアイデアが実際には機能せず、すべてを変更する必要があることに気づくかもしれません。

心配しないでください。すべてのバグは見つけることができますし、別の解決策を見つけることで、あなたのプラグインがさらに良くなるかもしれません。

テストを書くことから始めましょう。プラグインのボイラープレートには、 index.test.js にテストテンプレートがあります。プラグインをテストするには、npx jest を呼び出します。

コードをデバッグするには、テキストエディタでNode.jsデバッガを使用するか、単に console.log を使用します。

PostCSSコミュニティは、皆が同じ問題を経験しているので、あなたを助けることができます。恐れずに、special Gitter channelで質問してください。

Step 6: 公開

プラグインが完成したら、レポジトリでnpx clean-publishを呼び出しましょう。

clean-publish はnpmパッケージから開発設定を取り除くためのツールです。これはすでに我々の用意したボイラープレートに組み込まれています。

@postcssへのメンション付きでプラグインについて呟いたり、チャットルームでプラグインについて呟いたりしてくれれば、我々があなたのプラグインの宣伝を手伝います!

PostCSSプラグインの一覧に登録したい場合はAdd your new pluginを参照してください!