SolidJS + vite + tailwindcss な環境を作る(Storybook, vitest対応)

SolidJS + vite + tailwindcss な環境を作る(Storybook, vitest対応)

先日公開された State of JS 2021 を見ていて、 SolidJS という気になるフレームワークを見つけたので、少し齧ってみようと思いました。
まだ情報が少ない、かつ、 テストツールや Storybook などのツールを併用するにはまだひと工夫必要だったので、その対応方法と実際に作成したテンプレートについてサクッとまとめておきます。

SolidJS とは

SolidJS
React や Vue などの Front-end フレームワークの 1 つです。

すごく雑にまとめると、 Svelte と React の融合みたいなものです...!
昨今の Web Frontend では、 React が使われる事が多いなという感覚です。
SolidJS は、 React の宣言的な書き味を持ちつつ、 Svelte のように実 DOM を操作するパフォーマンス性を兼ね備えています。

import { createSignal, onCleanup } from 'solid-js'
import { render } from 'solid-js/web'

const CountingComponent = () => {
  const [count, setCount] = createSignal(0)
  const interval = setInterval(() => setCount(count => count + 1), 1000)
  onCleanup(() => clearInterval(interval))
  return <div>Count value is {count()}</div>
}

render(() => <CountingComponent />, document.getElementById('app'))

React から入った自分は、 Svelte の書き味に慣れず... SolidJS が v1 として公開されたタイミングで、シンプルに「良さそう...!」と思いました。
(どのフレームワークも思想がしっかりしていて、選択肢が増えたという気持ちです)

どんな構成を作ったか

今回作成したテンプレートは、以下の構成で作成しました。
開発体験として実用的な環境を構築できそうかという観点で作成しています。

  • SolidJS
  • TypeScript
  • tailwindcss
  • vite
  • vitest
  • ESlint
  • Prettier
  • Storybook
  • pnpm

決してイケイケだからという理由ではありません...!
自分のキャッチアップも兼ねています。本当です。

作成したテンプレートの完成形はこちら starter-for-solid です。

環境を作っていく

1 から環境を作っていきます。
この辺は公式ドキュメントを見ながらでスイスイでした。
パッケージマネージャーには pnpm を使っています。適宜 npmyarn に読み替えてください。

package.json を用意する

まずはここから。
環境を用意するディレクトリで、以下のコマンドを実行。

$ pnpm init

結果

.
`-- package.json

SolidJS 実行に最低限必要なものを入れる

まずは SolidJS を実行できる環境を用意します。
以下のコマンドで install します。

$ pnpm add solid-js
$ pnpm add -D typescript vite vite-plugin-solid

今回 TypeScript を使うため、 tsconfig.json を作成します。

$ pnpm tsc --init

以下に書き換える。
適宜好きなオプションに書き換えてください。

// tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "es5",
    "module": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "preserve", // ここ大事
    "jsxImportSource": "solid-js", // ここ大事
    "moduleResolution": "node",
    "newLine": "lf",
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "resolveJsonModule": true,
    "types": ["vite/client"],
    "skipLibCheck": true,
    "strict": true
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

vite で SolidJS を扱えるようにします。

// vite.config.ts

import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solidPlugin()],
})

プロジェクトルートに index.html を作成します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
    <title>Solid App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>

    <script src="/src/index.tsx" type="module"></script>
  </body>
</html>

/src/index.tsx にコンポーネントを作成します。
(React の感覚のまま jsx が使えるの、良い...!)

// ./src/index.tsx

import { createSignal, onCleanup } from 'solid-js'
import { render } from 'solid-js/web'

const CountingComponent = () => {
  const [count, setCount] = createSignal(0)
  const interval = setInterval(() => setCount(count => count + 1), 1000)
  onCleanup(() => clearInterval(interval))
  return <div>Count value is {count()}</div>
}

render(() => <CountingComponent />, document.getElementById('root'))

package.jsonscripts に実行コマンドを追加して実行してみる。

// package.json

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}
$ pnpm dev

localhost:3000 にアクセスすると、表示ができていることが確認できるかと思います。
基本的にはこれだけです。次に tailwindcss の設定をしていきます。

tailwindcss を使えるようにする

必要なものは以下です。インストールしていきましょう!

$ pnpm add -D tailwindcss postcss autoprefixer

次に tailwind.config.jspostcss.config.js を作成します。

// tailwind.config.js

module.exports = {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  darkMode: 'media',
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

// postcss.config.json

module.exports = {
  plugins: [require('tailwindcss'), require('autoprefixer')],
}

後は /src/index.css と、それを App で読み込ませれば OK です。
試しに文字色を赤にして反映されているか確認してみます。

// ./src/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;
// ./src/index.tsx

import { createSignal, onCleanup } from 'solid-js'
import { render } from 'solid-js/web'
import './index.css'

const CountingComponent = () => {
  const [count, setCount] = createSignal(0)
  const interval = setInterval(() => setCount(count => count + 1), 1000)
  onCleanup(() => clearInterval(interval))
  return <div class="text-red-500">Count value is {count()}</div>
}

render(() => <CountingComponent />, document.getElementById('root'))

文字色が変わっていれば OK です。

ここからは、開発時によく使うエコシステムとの連携をしていきます。

RSLint, prettier を追加する

この辺りも難しくはありません。
SolidJS も jsx を扱えるので、 React 同様の設定でいけます。

SolidJS 用の eslint-plugin-solid を追加するくらいの差分です。

必要なものをインストールします。
適宜ルールの追加や設定を行ってください。
※本テンプレートでは eslint-plugin-jsx-a11yeslint-plugin-import を追加しています。

$ pnpm add -D eslint eslint-plugin-solid eslint-config-prettier prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser

.eslintrc.js.prettierrc を作成します。

// .eslintrc.js

module.exports = {
  root: true,
  env: {
    node: true,
    es2022: true,
  },
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  ignorePatterns: ['node_modules/*'],
  extends: ['eslint:recommended'],
  overrides: [
    {
      files: ['**/*.ts', '**/*.tsx'],
      parser: '@typescript-eslint/parser',
      env: {
        browser: true,
      },
      extends: [
        'plugin:@typescript-eslint/recommended',
        'plugin:solid/typescript',
        'eslint-config-prettier',
      ],
    },
  ],
}
// .prettierrc

{
  "arrowParens": "avoid",
  "semi": false,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2
}

package.jsonscripts にコマンドを追加しておきます。

// package.json

{
  "scripts": {
    "format": "prettier --check './src/**/*.{ts,tsx,json}'",
    "format:fix": "prettier --write './src/**/*.{ts,tsx,json}'",
    "lint": "eslint './src/**/*.{ts,tsx}'",
    "lint:fix": "eslint --fix './src/**/*.{ts,tsx}'"
  }
}

pnpm lintpnpm format コマンドを実行してみると、上手く効くと思います。

余談ですが、 ESLint の pluginsextends は以下のことを行っています。

  • plugins
    ルールの追加
  • extends
    ルールの設定、その他

eslint-plugin-solid の README を見てみると、 "plugins": ["solid"] の記述がありますが、 plugin:solid/recommended 及び plugin:solid/typescript の中で plugins: ["solid"] の設定もしてくれているので、プロジェクトの .eslintrc.js 側では省略可能です。
FYI: https://github.com/joshwilsonvu/eslint-plugin-solid/blob/main/src/index.ts#L33-L90
把握していると捗ります!!

躓いたポイント

後は Storybook とテスト環境を用意するだけなのですが、この辺りで少し躓いたのでその備忘録を記載しておきます。

Storybook

SoliJS のコンポーネントを Storybook でデバッグするための設定をしている時に躓きました。。
結論としては、babel preset に babel-preset-solid を使うことで解決できました。
また、 tailwindcss も使えるように @storybook/addon-postcss を追加する必要があります。

Storybook デフォルトでは、トランスパイルに Babel が使われます。
つまり、 SolidJS 構文を Babel がトランスパイル可能な設定を行えば良いことになります。

本テンプレートはデフォルト設定のまま、 webpack4 による build 、 babel によるトランスパイルとしています。(2022/03 時点)
将来的には Storybook の builder を vite に統一しようと思っています...!

まずは必要なライブラリをインストールします。
適宜アドオンの追加や設定を行ってください。
※本テンプレートでは @storybook/addon-a11y を追加しています。

$ pnpm add -D @storybook/html @storybook/addon-essentials @storybook/addon-postcss babel-preset-solid @babel/preset-typescript

.storybook ディレクトリに main.jspreview.js を作成します。

// ./.storybook/main.js

module.exports = {
  stories: ['../src'],
  addons: [
    '@storybook/addon-essentials',
    {
      name: '@storybook/addon-postcss',
      options: {
        postcssLoaderOptions: {
          implementation: require('postcss'),
        },
      },
    },
  ],
  // babelrc でも良いですが、他に babel config を流用している箇所が無いため、Storybook の設定に書いています。
  babel: async options => ({
    ...options,
    presets: ['solid', '@babel/preset-typescript'],
  }),
}

// ./.storybook/preview.js

import '../src/index.css' // tailwindcss 用

package.jsonscripts にコマンドを追加しておきます。

// package.json

{
  "scripts": {
    "storybook": "start-storybook -p 6006"
  }
}

src ディレクトリ以下に、何かしら *.stories.tsx を用意して pnpm storybook コマンドを実行してみると、 Storybook で SolidJS のコンポーネントが確認できると思います。

テストツール

テストが書けることは必須です。
本テンプレートにもテストを書ける基盤を用意しました。

躓いた点は、 jest を使おうとしていて、その設定が上手く出来ませんでした。。
これは圧倒的に jest 力が足りませんでした。

まず最初に SolidJS でテストを書きたいと思ったときに、普段使っている jest を選びました。
公式にも solid-jest というライブラリが用意されており、これを使えばいけそうと安直に考えていました。
結果、うまくいかず...グヌヌ

発送を切り替え、 vite によるビルド設定を行っているので、そのまま vitest を使えないかと思いました。
そしたら意外とすんなりいけたので、その設定を記載しておきます。

まずは必要なライブラリをインストールします。

$ pnpm add -D vitest jsdom

tsconfig.jsontypesvitest/global を設定します。

// tsconfig.json

{
  "types": ["vite/client", "vitest/globals"]
}

vite.config.ts に以下のように変更します。

// vite.config.ts

import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solidPlugin()],
  // ここ以下を追加
  test: {
    environment: 'jsdom',
    transformMode: {
      web: [/\.[jt]sx?$/],
    },
    deps: {
      inline: [/solid-js/],
    },
  },
  resolve: {
    conditions: ['development', 'browser'],
  },
})

package.jsonscripts にコマンドを追加しておきます。

// package.json

{
  "scripts": {
    "test": "vitest --passWithNoTests" // test ファイルが無いときにエラーにしないオプションを付けています
  }
}

これで vitest を使って SolidJS のテストを書くことが出来ます。

ここまでで SolidJS を触る準備が整いました!
自分もこれから触っていきます...!

まとめ

今回は SolidJS の v1 公開を知って、 SolidJS を触るためのテンプレートを作成する手順を記載しました。

まだガッツリ触ったわけでは無いので、設定不備が見つかる可能性はあります。ご了承ください :pray:
SolidJS は SSR も行うことが可能です。
今回は React チックに CSR の SPA の用途を想定しているため、 SSR したい場合は設定がいくつか異なります。

今回作成したテンプレートも都度アップデートを行っていく予定です。
知見のある方いましたら issue や DM でご教示頂けると嬉しいです!

作成したテンプレート:https://github.com/hey3/starter-for-solid

Previous
AWS CDK で lambda のバージョンが上手く発行できない時の対処