LoginSignup
4

More than 3 years have passed since last update.

Next.js10用 Boilerplate(Typescript, Sass, Tailwind.css, ESLint/Prettier, Storybook, jest, Hygen )

Posted at

久しぶりに Next.js で新規プロジェクトを作ろうと触ってみたところ、結構変わっていた。
新しくBoilerplateを検討してみた際のメモです。

この記事で扱う内容

  • Next.jsのインストール
  • Typescriptの導入
  • sassの導入
  • Tailwind.cssの導入
  • ESLint/Prettierの導入
  • Storybookの導入
  • jestの導入
  • コンポーネントの読み込み用エイリアスを設定
  • Hygenテンプレート

Next.jsのインストール

こちらを参考に

% npx create-next-app <アプリ名>
% cd <アプリ名> 
% npm run dev
  • http://localhost:3000/ にアクセスし起動することを確認。

Typescriptの導入

必要パッケージを追加し、configを作成。

% npm i -D typescript @types/react @types/node @types/react-dom
# tsconfig.jsonを生成
% tsc --init 

以下の手順で動作を確認。

  • tsconfig.json 内、 "strict": true, にする。
  • /pages 配下の .js ファイルを全て .tsx に変更する
  • http://localhost:3000/ にアクセスし起動することを確認。

Sass の導入

react.js内では、cssをどの様に実装するかはいくつかの選択肢があるが、next.jsはデフォルトで Build-in CSS がサポートされている。sassについてもパッケージの追加だけでOK。

公式が推奨しており、CSS in JS などよりはパフォーマンスの面でも有利らしいので、現状はこのまま採用するのが良さそう。

% npm install sass

これだけで .modules.scss ファイルを使用できるようになる。

Tailwind.cssの導入

こちら に沿って導入。

% npm install tailwindcss@latest postcss@latest autoprefixer@latest
% npx tailwindcss init -p

tailwind.config.jspostcss.config.js を生成。

tailwind.config.jspurge オプションに、.tsxを配置するdirを設定する。
コンポーネントは ./src/components/** 配下に配置していく想定。

tailwind.config.js
module.exports = {
  purge: ['./pages/**/*.{js,ts,tsx}', './src/components/**/*.{js,ts,tsx}'],
  ...
}

その他、必要なスタイル設定があれば追記していく。

/pages/_app.tsx の冒頭で import すれば使えるようになる。

/pages/_app.tsx
import 'tailwindcss/tailwind.css'

その他 必要に応じて

  • ベンダープレフィックスが必要 → postcss.config.jsonautoprefixer を追加。参考こちら
  • 変数を使いたい → 基本的には CSS Variables で対応することを念頭に開発する。
  • scssの変数をどうしても使いたい → @use,@forwardを使う (dart-sass@import は廃止予定)
  • scss variables@use で使うには tsconfig.json に path alias を設定しておくとよい

※ scss variables 用の path alias

ざっくりこんな感じでtsconfig.json に path alias を設定しておくと便利。

tsconfig.json
{
  "compilerOptions": {
     ...
    "paths": {
      "@variables": ["./src/styles/_variables.scss"],
    },
    ...
  },
  ...
}

呼び出す場合は

example.modules.scss
@use "@variables" as v;

.hoge {
  color: v.$color-primary;
}

ESLint, Prettierの導入

必要パッケージを追加し、configを作成。

# ESLint関係のパッケージ
% npm i -D eslint @typescript-eslint/parser  @typescript-eslint/eslint-plugin  eslint-plugin-react  eslint-plugin-react-hooks  eslint-plugin-jsx-a11y  eslint-plugin-import  eslint-import-resolver-typescript

# prettier関連のパッケージ
% npm i -D prettier eslint-plugin-prettier eslint-config-prettier

.eslintrc.js を作成。ざっくり以下のように

eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
    'prettier/react',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 2020,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'react', 'import'],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      },
      typescript: {
        config: 'tsconfig.json',
        alwaysTryTypes: true,
      },
    },
    react: {
      version: 'detect',
    },
  },
  rules: {
    'import/no-unresolved': 'off',
    '@typescript-eslint/ban-types': [
      'error',
      {
        types: {
          '{}': false,
        },
      },
    ],
    'react/prop-types': ['off'],
    'react/react-in-jsx-scope': 'off',
    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
    'import/order': ['error'],
    'prettier/prettier': [
      'error',
      {
        trailingComma: 'all',
        endOfLine: 'lf',
        semi: false,
        singleQuote: true,
        printWidth: 80,
        tabWidth: 2,
      },
    ],
    'jsx-a11y/anchor-is-valid': [
      'error',
      {
        components: ['Link'],
        specialLink: ['hrefLeft', 'hrefRight'],
        aspects: ['invalidHref', 'preferButton'],
      },
    ],
  },
}

対象にしたくないファイルは .eslintignore を作り、これに追記する。

package.json に 起動するための script を追加

package.json
{
  "scripts": {
    "lint": "eslint --ext .js,.jsx,.ts,.tsx .",
    "lintfix": "eslint --ext .js,.jsx,.ts,.tsx --fix .",
  },
}

Storybookの導入

下記コマンドで、next.jsの構成を自動検知し、パッケージ追加と初期設定を行ってくれる。

% npx sb init

.storybook/main.js を開き *.stories.mdx へのパスを調整、 使う addons を追加する。

またこのままでは scss variables 用の path alias がうまく読み込めない為 webpack.config.js を追加する。
../src/styles/variables.scss にある 変数を @variables というエイリアスで各コンポーネントから参照できるようにする。

% npm i -D sass css-loader sass-loader style-loader babel-preset-react-app
webpack.config.js
const path = require('path')
module.exports = ({ config }) => {
  config.resolve.alias = {
    ...config.resolve.alias,
    '@variables': path.resolve(__dirname, '../src/styles/variables.scss')
  };
  config.module.rules.push({
    test: /\.scss$/,
    loaders: ['style-loader', 'css-loader', 'sass-loader'],
    include: path.resolve(__dirname, '../')
  });
  config.module.rules.push({
    test: /\.css$/,
    loader: 'style-loader!css-loader',
    include: __dirname
  });
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve('babel-loader'),
    options: {
      presets: [require.resolve('babel-preset-react-app')],
    },
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
};

(※ エラーが出る場合はバージョンをチェック。ダウングレードしないとうまく動かないものがある。エラーを見て細かくバージョンをチェックして対応する。)

package.json に 起動するための script を追加

package.json
{
  "scripts": {
    "storybook": "start-storybook -p 6006"
  },
}

jest の導入

必要パッケージを追加。

% npm i -D jest ts-jest @types/jest react-test-renderer enzyme enzyme-to-json @wojtekmaj/enzyme-adapter-react-17 @types/react-test-renderer @types/enzyme
% npx ts-jest config:init

(※ @wojtekmaj/enzyme-adapter-react-17 は、Enzyme が React 17 に対応したら不要になる様子。)

必要なコンフィグ類を作成。

jest.config.js に追記。test 時に CSS ファイルを読み込まないようにする設定も加える。

jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: [
    '**/__tests__/**/*.+(ts|tsx|js)',
    '**/?(*.)+(spec|test).+(ts|tsx|js)',
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  moduleNameMapper: {
    '^src/(.*)$': '<rootDir>/src/$1',
    '\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
  },
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  snapshotSerializers: ['enzyme-to-json/serializer'],
  setupFilesAfterEnv: ['<rootDir>/test/setupTests.ts'],
  globals: {
    'ts-jest': {
      tsconfig: '<rootDir>/test/tsconfig.jest.json',
    },
  },
}

test/tsconfig.jest.jsontest/setupTests.ts を作成。

test/tsconfig.jest.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "jsx": "react"
  }
}
test/setupTests.ts
import Enzyme from 'enzyme'
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'

Enzyme.configure({ adapter: new Adapter() })

package.json に 起動するための script を追加。

package.json
{
  "scripts": {
    "test": "jest"
  },
}

コンポーネントが .css を読み込んでいるとエラーが出る。jest-css-modulesを追加。

% npm i -D jest-css-modules

jest.config.js に以下を追加

jest.config.js
module.exports = {.
  moduleNameMapper: {
    '\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules'
  },
}

コンポーネントの読み込み用エイリアスを設定

Next.jsのデフォルトでは、コンポーネントのimportの際、 ../../ のような相対パスで書く必要がある。
通常は next.config.js に エイリアスを追加するが、今回はTypescriptを使っているので、 tsconfig.json に設定する。
~@ を以下の様に追加する。

tsconfig.json
{
  "compilerOptions": {
    ...
    "paths": {
      "@/*": ["./*"],
      "~/*": ["./*"],
    },
    ...
  },
  ...
}

読み出し側では以下のように。

pages/index.tsx
import ComponentName from '@/src/components/path/to/ComponentName'
import ComponentName from '~/src/components/path/to/ComponentName'

hygenテンプレート

ここまでで設定してきたものをすべて手動で扱うのは一苦労なので、hygenで自動化する。

% npm i -D hygen

ここで % hygen init self すると、サンプルファイルが /_templates/generator/ に作られる。
これを削除し、同箇所に設定ファイルを作成するのだが、今回は .hygen/ 以下に置きたいので .hygen.jsを追加する。

hygen.js
module.exports = {
  templates: `${__dirname}/.hygen`,
}

これで、テンプレートファイルは .hygen/ に配置できる。

一例として、コンポーネントを新規作成する際のテンプレートを作ってみる。
以下のような設定ファイルを作成する。

.hygen
└── new
    └── component
        ├── prompt.js
        ├── scss.ejs.t
        ├── story.ejs.t
        ├── test.ejs.t
        └── tsx.ejs.t

各ファイルは以下のような感じで。

hygen/new/component/prompt.js
module.exports = [
  {
    type: 'select',
    name: 'category',
    message: '作成するcomponentの粒度を選んでください',
    choices: ['atoms', 'molecules', 'organisms', 'templates'],
  },
  {
    type: 'input',
    name: 'componentname',
    message: 'component名をパスカルケースで入力してください',
    hint: '"MyButton"など',
  },
]
hygen/new/component/scss.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.module.scss
---

@use "@variables" as v;

.root {}
hygen/new/component/story.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.stories.tsx
---
import React from 'react'
import { Story, Meta } from '@storybook/react'
import { <%= componentname %>, Props } from './<%= componentname %>'

export default {
  component: <%= componentname %>,
  title: '<%= category %>/<%= componentname %>',
} as Meta

const Template: Story<Props> = (args) => <<%= componentname %> {...args} />

export const Default = Template.bind({})
Default.args = {}
hygen/new/component/test.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.spec.tsx
---
import React from 'react'
import { shallow } from 'enzyme'
import { <%= componentname %> } from './<%= componentname %>'

describe('<%= componentname %> test', () => {
  const wrapper = shallow(<<%= componentname %>></<%= componentname %>>)

  it('renders correctly', () => {
    expect(wrapper).toMatchSnapshot()
  })
})
hygen/new/component/tsx.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.tsx
---

import React from 'react'
import styles from './<%= componentname%>.module.scss'

export type Props = {}

export const <%= componentname%>: React.VFC<Props> = ({}) => <div className={styles.root}></div>

以下のコマンドで対話を起動。

% hygen new component

対話に沿って、例えば atoms/MyComponentName を作成すると、 src/components/atoms/ に以下のファイルが生成される。
この例は非常にシンプルだが、promptを工夫する事で、より細かい出し分けや、作り込みが可能になる。

参考

参考にさせていただきました。ありがとうございます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4