Open9
【Next.js】App Routerが出てきた今、もう一度環境構築について一考する
プロジェクトの作成
今回はpnpmでプロジェクトを作成します
mkdir next-app-router-template
cd next-app-router-template
pnpm dlx create-next-app .
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … Yes
✔ Would you like to use `src/` directory with this project? … Yes
✔ Use App Router (recommended)? … Yes
✔ Would you like to customize the default import alias? … Yes
✔ What import alias would you like configured? … @/*
オプションはすべてYesにします
tsconfigの設定
規約はガチガチにしたいので、@tsconfig/strictest
を導入します
pnpm add -D @tsconfig/strictest
tsconfig.json
{
+ "extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
拡張子の変更
tailwind.config.js
やpostcss.config.js
、next.config.js
といった設定ファイルの拡張子をcjs
に変更します。ファイルは CommonJS モジュールです。ES モジュールに変換される可能性があります。
というエラーが鬱陶しいからです。ついでに.eslintrc.json
も.eslintrc.cjs
に変更します。
.eslintrc.cjs
module.exports = {
extends: ["next/core-web-vitals"],
};
ESLint & Prettier
必要なライブラリをインストール
pnpm add -D eslint-config-airbnb eslint-plugin-jsx-a11y @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import eslint-plugin-unused-imports eslint-config-prettier prettier
.prettierrc
を追加
.prettierrc
{
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"quoteProps": "consistent",
"trailingComma": "es5",
"jsxSingleQuote": false,
"bracketSpacing": true,
"arrowParens": "always"
}
.eslintrc.cjs
を編集
.eslintrc.cjs
module.exports = {
root: true,
extends: [
'eslint:recommended',
'airbnb',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'next/core-web-vitals',
'prettier',
],
plugins: ['import', 'unused-imports', 'jsx-a11y', '@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
},
rules: {
/* eslint */
'arrow-body-style': 'off',
/* typescript */
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
},
],
/* react */
'react/jsx-filename-extension': [
'error',
{ extensions: ['.js', '.jsx', 'ts', 'tsx'] },
],
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/function-component-definition': 'off',
/* import */
'unused-imports/no-unused-imports': 'error',
'import/order': [
'error',
{
'groups': [
'builtin',
'external',
'parent',
'sibling',
'index',
'object',
'type',
],
'pathGroupsExcludedImportTypes': ['builtin'],
'newlines-between': 'always',
'pathGroups': [
{
pattern: '{react,react-dom/**,react-router-dom}',
group: 'builtin',
position: 'before',
},
{
pattern: '@/app/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/components/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/stores/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/providers/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/hooks/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/constants/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/libs/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/utils/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/**',
group: 'parent',
position: 'before',
},
],
'alphabetize': {
order: 'asc',
},
},
],
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
},
],
'no-restricted-imports': [
'error',
{
patterns: ['./*', '../*', '~/*', '~~/*'],
},
],
},
};
.eslintignore
を追加
# config
.eslintrc.cjs
.prettierrc
next.config.cjs
tailwind.config.cjs
tsconfig.json
postcss.config.cjs
# build
build/
bin/
obj/
out/
.next/
参考
Linterの設定を追加
package.json
{
"name": "next-app-router-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "eslint:fix": "eslint src --ext .js,jsx,.ts,.tsx --fix",
+ "prettier:fix": "prettier --check --write 'src/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}'",
+ "format": "pnpm run eslint:fix && pnpm run prettier:fix"
},
"dependencies": {
"@types/node": "20.2.5",
"@types/react": "18.2.8",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"eslint": "8.42.0",
"eslint-config-next": "13.4.4",
"next": "13.4.4",
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.8.8"
}
}
.vscode/settings.json
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true,
"source.fixAll.eslint": true,
"source.organizeImports": false
},
"css.validate": false,
"scss.validate": false,
"javascript.format.enable": false,
"typescript.format.enable": false
}
Markuplintの設定
pnpm dlx markuplint --init
✔ Which do you use template engines? · React (JSX)
✔ May I install them automatically? (y/N) · false
✔ Do you customize rules? (y/N) · false
✔ Does it import the recommended config? (y/N) · true
pnpm add -D markuplint @markuplint/jsx-parser @markuplint/react-spec
package.json
{
"name": "next-app-router-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"eslint:fix": "eslint src --ext .js,jsx,.ts,.tsx --fix",
"prettier:fix": "prettier --check --write 'src/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}'",
- "format": "pnpm run eslint:fix && pnpm run prettier:fix"
+ "format": "pnpm run eslint:fix && pnpm run prettier:fix",
+ "html:lint": "markuplint src/**/*.{html,jsx,tsx}"
},
"dependencies": {
"@types/node": "20.2.5",
"@types/react": "18.2.8",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"eslint": "8.42.0",
"eslint-config-next": "13.4.4",
"next": "13.4.4",
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
},
"devDependencies": {
"@markuplint/jsx-parser": "^3.7.0",
"@markuplint/react-spec": "^3.8.0",
"@tsconfig/strictest": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-unused-imports": "^2.0.0",
"markuplint": "^3.10.0",
"prettier": "^2.8.8"
}
}
TailwindCSSの設定
.vscode/settings.json
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true,
"source.fixAll.eslint": true,
"source.organizeImports": false
},
"css.validate": false,
"scss.validate": false,
"javascript.format.enable": false,
- "typescript.format.enable": false
+ "typescript.format.enable": false,
+
+ "files.associations": {
+ "*.css": "tailwindcss"
+ }
}
tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
};
src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
linter用のライブラリをインストール
pnpm add -D eslint-plugin-tailwindcss prettier-plugin-tailwindcss
.eslintrc.cjs
module.exports = {
root: true,
extends: [
'eslint:recommended',
'airbnb',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'next/core-web-vitals',
+ 'plugin:tailwindcss/recommended',
'prettier',
],
- plugins: ['import', 'unused-imports', 'jsx-a11y', '@typescript-eslint'],
+ plugins: [
+ 'import',
+ 'unused-imports',
+ 'jsx-a11y',
+ '@typescript-eslint',
+ 'tailwindcss',
+ ],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
},
rules: {
/* eslint */
'arrow-body-style': 'off',
/* typescript */
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
},
],
/* react */
'react/jsx-filename-extension': [
'error',
{ extensions: ['.js', '.jsx', 'ts', 'tsx'] },
],
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/function-component-definition': 'off',
/* import */
'unused-imports/no-unused-imports': 'error',
'import/order': [
'error',
{
'groups': [
'builtin',
'external',
'parent',
'sibling',
'index',
'object',
'type',
],
'pathGroupsExcludedImportTypes': ['builtin'],
'newlines-between': 'always',
'pathGroups': [
{
pattern: '{react,react-dom/**,react-router-dom}',
group: 'builtin',
position: 'before',
},
{
pattern: '@/app/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/components/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/stores/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/providers/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/hooks/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/constants/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/libs/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/utils/**',
group: 'parent',
position: 'before',
},
{
pattern: '@/**',
group: 'parent',
position: 'before',
},
],
'alphabetize': {
order: 'asc',
},
},
],
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
},
],
'no-restricted-imports': [
'error',
{
patterns: ['./*', '../*', '~/*', '~~/*'],
},
],
+ /* tailwindcss */
+ 'tailwindcss/no-custom-classname': [
+ 'warn',
+ {
+ config: 'tailwind.config.cjs',
+ },
+ ],
+ 'tailwindcss/classnames-order': 'off',
},
};
Jest
pnpm add -D jest jest-environment-jsdom @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/testing-library__jest-dom
設定ファイルの作成
jest.config.cjs
const nextJest = require('next/jest')
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
jest.setup.js
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
package.json
{
"name": "next-app-router-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"eslint:fix": "eslint src --ext .js,jsx,.ts,.tsx --fix",
"prettier:fix": "prettier --check --write 'src/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}'",
"format": "pnpm run eslint:fix && pnpm run prettier:fix",
- "html:lint": "markuplint src/**/*.{html,jsx,tsx}"
+ "html:lint": "markuplint src/**/*.{html,jsx,tsx}",
+ "test": "jest --watch",
+ "test:ci": "jest --ci"
},
"dependencies": {
"@types/node": "20.2.5",
"@types/react": "18.2.8",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"daisyui": "^3.0.3",
"eslint": "8.42.0",
"eslint-config-next": "13.4.4",
"next": "13.4.4",
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
},
"devDependencies": {
"@markuplint/jsx-parser": "^3.7.0",
"@markuplint/react-spec": "^3.8.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@tsconfig/strictest": "^2.0.1",
"@types/testing-library__jest-dom": "^5.14.6",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-tailwindcss": "^3.12.1",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"markuplint": "^3.10.0",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0"
}
}
Storybook
pnpm dlx storybook@latest init
.eslintrc.cjs
module.exports = {
root: true,
extends: [
'eslint:recommended',
'airbnb',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'next/core-web-vitals',
'plugin:tailwindcss/recommended',
+ 'plugin:storybook/recommended',
'prettier',
],
...
};
設定ファイルの編集
.storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/app/globals.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;