久しぶりに 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.js
と postcss.config.js
を生成。
tailwind.config.js
の purge
オプションに、.tsx
を配置するdirを設定する。
コンポーネントは ./src/components/**
配下に配置していく想定。
module.exports = {
purge: ['./pages/**/*.{js,ts,tsx}', './src/components/**/*.{js,ts,tsx}'],
...
}
その他、必要なスタイル設定があれば追記していく。
/pages/_app.tsx
の冒頭で import すれば使えるようになる。
import 'tailwindcss/tailwind.css'
その他 必要に応じて
- ベンダープレフィックスが必要 →
postcss.config.json
にautoprefixer
を追加。参考こちら。 - 変数を使いたい → 基本的には
CSS Variables
で対応することを念頭に開発する。 -
scss
の変数をどうしても使いたい →@use
,@forward
を使う (dart-sass
で@import
は廃止予定) -
scss variables
を@use
で使うにはtsconfig.json
に path alias を設定しておくとよい
※ scss variables 用の path alias
ざっくりこんな感じでtsconfig.json
に path alias を設定しておくと便利。
{
"compilerOptions": {
...
"paths": {
"@variables": ["./src/styles/_variables.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
を作成。ざっくり以下のように
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 を追加
{
"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
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 を追加
{
"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 ファイルを読み込まないようにする設定も加える。
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.json
と test/setupTests.ts
を作成。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}
import Enzyme from 'enzyme'
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'
Enzyme.configure({ adapter: new Adapter() })
package.json
に 起動するための script を追加。
{
"scripts": {
"test": "jest"
},
}
コンポーネントが .css を読み込んでいるとエラーが出る。jest-css-modules
を追加。
% npm i -D jest-css-modules
jest.config.js
に以下を追加
module.exports = {.
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules'
},
}
コンポーネントの読み込み用エイリアスを設定
Next.jsのデフォルトでは、コンポーネントのimportの際、 ../../
のような相対パスで書く必要がある。
通常は next.config.js
に エイリアスを追加するが、今回はTypescriptを使っているので、 tsconfig.json
に設定する。
~
や @
を以下の様に追加する。
{
"compilerOptions": {
...
"paths": {
"@/*": ["./*"],
"~/*": ["./*"],
},
...
},
...
}
読み出し側では以下のように。
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
を追加する。
module.exports = {
templates: `${__dirname}/.hygen`,
}
これで、テンプレートファイルは .hygen/
に配置できる。
一例として、コンポーネントを新規作成する際のテンプレートを作ってみる。
以下のような設定ファイルを作成する。
.hygen
└── new
└── component
├── prompt.js
├── scss.ejs.t
├── story.ejs.t
├── test.ejs.t
└── tsx.ejs.t
各ファイルは以下のような感じで。
module.exports = [
{
type: 'select',
name: 'category',
message: '作成するcomponentの粒度を選んでください',
choices: ['atoms', 'molecules', 'organisms', 'templates'],
},
{
type: 'input',
name: 'componentname',
message: 'component名をパスカルケースで入力してください',
hint: '"MyButton"など',
},
]
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.module.scss
---
@use "@variables" as v;
.root {}
---
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 = {}
---
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()
})
})
---
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を工夫する事で、より細かい出し分けや、作り込みが可能になる。
参考
参考にさせていただきました。ありがとうございます。
- https://nextjs.org/learn/basics/create-nextjs-app/setup
- https://nextjs.org/docs/basic-features/built-in-css-support
- https://jestjs.io/ja/
- https://storybook.js.org/
- https://www.hygen.io/
- https://tailwindcss.com/docs/guides/nextjs
- https://qiita.com/tatane616/items/e3ee99a181662ad6824b
- https://qiita.com/282Haniwa/items/dcce1ba6bb6ae541893e
- https://qiita.com/syuji-higa/items/931e44046c17f53b432b
- https://qiita.com/github0013@github/items/303a32d3037d322e67c0
- https://qiita.com/tatane616/items/e3ee99a181662ad6824b
- https://qiita.com/taroodr/items/19c852aa5d43c1c587fb
- https://qiita.com/daku10/items/c00d087458f2f1de6545
- https://qiita.com/tashirimo_dev/items/fb29bd4d0318b0909d0b
- https://qiita.com/numa999/items/aef01affdb25bbaa3290
- https://zenn.dev/catnose99/scraps/5e3d51d75113d3
- https://zenn.dev/matamatanot/articles/e0e6371fe28b7479fb3f
- https://zenn.dev/takepepe/articles/hygen-template-generator
- https://zenn.dev/takepepe/scraps/6668e9fe402666
- https://panda-program.com/posts/nextjs-storybook-typescript-errors#nextjs%E3%81%ABstorybook%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B
- https://tech.playground.style/javascript/next-js-typescript/