EmotionとSvelteを組み合わせる場合の設定とうまくいかない部分とか

CSS in JSライブラリのEmotionにはReact版とFramework agnostic版がある。なのでReact以外のビューライブラリと組み合わせたりバニラJSでも使ったりできるけど、Babelプラグインが実質必須なのでSingle File Componentsみたいなスタイルとは相性が悪い。

<script>
  import { css } from "emotion";
</script>

<h1 class={css({ color: "pink" })}>Hello!</h1>

EmotionのBabelプラグインはコードの最適化に加えてCSSのソースマップ生成を行う。Emotionが生成するクラス名はランダムなハッシュ値になって可読性がないので、デバッグするにはソースマップがかなり重要になる。Babelプラグインが正しく機能すれば開発者ツールで検出した宣言ブロックからソースコード内の定義元が参照できるようになる。

出典:Emotion - Source Maps

一方でSvelteコンポーネントはそのままではBabelでパースできないので、このBabelプラグインを使うには次のいずれかの方法を取るしかない:

  1. svelte-loaderあるいはrollup-plugin-svelteによってJavaScriptに変換された後にBabelを適用する
  2. Svelte Preprocessによってscript要素内のみにBabelを適用する

1の場合、JavaScriptコンパイルされた後のソースコードを基準にしてソースマップを生成するので、ユーザーが実際に編集するファイルと別のものを参照してしまう。

2の場合、script要素内のコードについては正しくソースマップが生成されるが、テンプレートにはBabelが適用されないので、テンプレートに直接記述した定義にはソースマップが生成されない。この場合テンプレート内の最適化も行われない。

またいずれにしてもSvelteコンポーネントの外でスタイルを定義してimportすればソースマップは正しく生成される。

import { css } from "emotion";

export const hello = css({ color: "pink" });
<script>
  import { hello } from "./styles";
</script>

<h1 class={hello}>Hello!</h1>

しかしこの使い方だとインラインスタイル風に書きたいというモチベーションが満たせないので自分としてはあまり意味がない。同じ理由でscript要素内にすべて定義してしまうのも無い。

そのためどちらかというと1の方がマシという結論。ソースマップがまったく無いよりは良いし、最適化も漏れなく適用させたい。webpack.config.jsはこんな感じ:

module: {
  rules: [
    ...
    {
      test: /\.svelte$/,
      use: [
        {
          loader: "babel-loader",
          options: {
            plugins: ["emotion"],
          },
        },
        {
          loader: "svelte-loader",
        },
      ],
    },
    ...
  ],
},

またVS Codeでは、Babel JavaScriptvscode-styled-componentsをインストールすることでTemplate literals内に記述したCSSシンタックスハイライトされたり補完が効くようになったりするが、Svelteコンポーネントのテンプレート中ではそれが有効にならない。script要素内では動く。

代わりにObject Stylesで記述すると特別なプラグインを足さずともまともに書けるようになるし、Emotionの型定義ファイルからプロパティ名や値を補完してくれる。最低限Svelte for VS Codeは必要。