Create Next App で生成される Next.js のプロジェクトは CRA で生成される内容と比べると不親切なので、快適に開発できるようにしようという内容です Zero Config?そんなものは知りません。TypeScript も ESLint も Prettier も Jest も全部欲しい!Next.js の examples では物足りない! VSCode で開発し TypeScript で SSG として組む前提で書いてます
確認環境
- CRA で吐き出される TypeScript 向けのプロジェクトに対し
npm run eject
をかけたものをベースにしてます - SCSS を使いたいので sass を入れています
Env | Ver |
---|---|
VSCode | 1.57.1 |
next | 11.0.1 |
react | 17.0.2 |
react-dom | 17.0.2 |
@testing-library/jest-dom | 5.14.1 |
@testing-library/react | 11.2.7 |
@testing-library/react-hooks | 7.0.1 |
@types/jest | 26.0.23 |
@types/node | 15.0.2 |
@types/react | 17.0.5 |
@types/react-dom | 17.0.3 |
@typescript-eslint/eslint-plugin | 4.23.0 |
@typescript-eslint/parser | 4.23.0 |
eslint | 7.26.0 |
eslint-config-next | 11.0.1 |
eslint-config-prettier | 8.3.0 |
eslint-config-react-app | 6.0.0 |
eslint-plugin-flowtype | 5.7.2 |
eslint-plugin-import | 2.23.2 |
eslint-plugin-jest | 24.3.6 |
eslint-plugin-jsx-a11y | 6.4.1 |
eslint-plugin-react | 7.23.2 |
eslint-plugin-react-hooks | 4.2.0 |
eslint-plugin-testing-library | 3.9.0 |
identity-obj-proxy | 3.0.0 |
jest | 27.0.5 |
sass | 1.38.2 |
jest-watch-typeahead | 0.6.4 |
prettier | 2.2.1 |
serve | 12.0.0 |
ts-jest | 27.0.3 |
typescript | 4.3.4 |
npm packages の導入
- 次のコマンドを流し必要なものを一式入れる
- バージョンの整合性が上手く合わないときは気合で調整する
npm i next react react-dom
npm i -D @testing-library/jest-dom @testing-library/react @testing-library/react-hooks @types/jest @types/node @types/react @types/react-dom @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-next eslint-config-prettier eslint-config-react-app eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jest eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-testing-library identity-obj-proxy jest jest-watch-typeahead prettier sass serve ts-jest typescript
プロジェクトの構成例
- 例なので、実際の使用に際しては個々の事情に応じていい感じにしてください
|- .vscode/ |- extensions.json |- launch.json |- settings.json |- public/ |- src/ |- components/ |- pages/ |- .eslintignore |- .eslintrc |- .gitignore |- .prettierrc |- jest-setup.ts |- jest.config.js |- next-env.d.ts |- next.config.js |- package.json |- tsconfig.jest.json |- tsconfig.json
.vscode/extensions.json
- 使う拡張機能を書いておくと拡張機能インストールの提案が走るのであると便利なやつです
{ "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "msjsdiag.debugger-for-chrome" ] }
.vscode/launch.json
- ブレークポイントを設定してデバッグするときの設定です
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Next: Node", "skipFiles": ["<node_internals>/**", ".next/**"], "port": 9229 }, { "type": "chrome", "request": "launch", "name": "Next: Chrome", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}", "sourceMapPathOverrides": { "webpack://_N_E/*": "${webRoot}/*" }, "runtimeArgs": ["--profile-directory=debug-profile"] } ], "compounds": [ { "name": "Next: Full", "configurations": ["Next: Node", "Next: Chrome"] } ] }
.vscode/settings.json
- VSCode の設定をワークスペースで上書くやつです
- 次の 2 つでワークスペースの TypeScript を使うことを宣言しています。グローバルとは切り離しておきたいことってあると思います
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib",
// Place your settings in this file to overwrite default and user settings. { "search.exclude": { "**/node_modules": true, "**/.next": true, "**/out": true, "**/coverage": true }, "files.eol": "\n", "files.insertFinalNewline": true, "git.suggestSmartCommit": false, "git.autofetch": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.alwaysShowStatus": true, "eslint.validate": ["javascript", "typescript"], "editor.formatOnSave": true, "[javascript]": { "editor.tabSize": 2 }, "javascript.suggest.paths": true, "javascript.updateImportsOnFileMove.enabled": "always", "[typescript]": { "editor.tabSize": 2 }, "typescript.suggest.paths": true, "typescript.updateImportsOnFileMove.enabled": "always", "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.tsdk": "node_modules/typescript/lib", "typescript.referencesCodeLens.enabled": true, "typescript.validate.enable": true, "typescript.preferences.importModuleSpecifier": "relative" }
public/
- こいつはルートに配置されている必要があり、src/ の下には配置できない
src/
- Next の公式テンプレでは基本的に存在しないが、ルートがカオスになるのであったほうがいいと思う
.eslintignore
- とりあえずこんくらいあればいいでしょう
# /node_modules/* in the project root is ignored by default # build artefacts .next/* out/* coverage/* # data definition files **/*.d.ts
.eslintrc
- TS と React の推奨を設定し、ビルドエラーに繋がりな部分や、型を厳格にする設定、楽な記法を全力採用
- Jest 周りが弱い気がしてるので、もっといい書き方があると思います
"extends": "next"
は意図的に入れていませんpackage.json
に書いてあるeslint-config-next
はないと怒られるので入れてるだけです
{ "parser": "@typescript-eslint/parser", "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "react-app", "react-app/jest", "plugin:react/recommended", "prettier" ], "plugins": ["react", "@typescript-eslint"], "parserOptions": { "project": "./tsconfig.json", "ecmaFeatures": { "jsx": true } }, "settings": { "react": { "version": "detect" } }, "rules": { // Enforce the consistent use of either backticks, double, or single quotes "quotes": ["warn", "single"], // Enforce the consistent use of either function declarations or expressions "func-style": ["warn", "expression", { "allowArrowFunctions": true }], // Detect missing key prop "react/jsx-key": [ "warn", { "checkFragmentShorthand": true, "checkKeyMustBeforeSpread": true } ], // Enforce shorthand or standard form for React fragments "react/jsx-fragments": ["warn", "syntax"], // Prevent missing React when using JSX "react/react-in-jsx-scope": "off", // Validate whitespace in and around the JSX opening and closing brackets "react/jsx-tag-spacing": [ "warn", { "closingSlash": "never", "beforeSelfClosing": "always", "afterOpening": "never", "beforeClosing": "never" } ], // Prevent extra closing tags for components without children "react/self-closing-comp": [ "warn", { "component": true, "html": true } ], // Allow non-explicit return types on functions and class methods "@typescript-eslint/explicit-function-return-type": "off", // Allow non-explicit return types on exported functions "@typescript-eslint/explicit-module-boundary-types": "off", // Disallow usage of the any type "@typescript-eslint/no-explicit-any": 1, // Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean "@typescript-eslint/no-inferrable-types": [ "error", { "ignoreParameters": true } ], // Error on declared but not used variable "@typescript-eslint/no-unused-vars": "error" } }
.gitignore
- これはテンプレそのままでいい気がします
# /node_modules/* in the project root is ignored by default # build artefacts .next/* out/* coverage/* # data definition files **/*.d.ts # config files **/*.config.js
.prettierrc
- こんくらいあればいいでしょう
{ "trailingComma": "es5", "printWidth": 80, "tabWidth": 2, "semi": true, "singleQuote": true }
jest-setup.ts
- CRA で React をセットアップすると当たり前のように入ってる matcher を標準で入れるためのやつです
- 具体的には
toHaveAttribute()
みたいなやつを追加できます
import '@testing-library/jest-dom'; import '@testing-library/jest-dom/extend-expect';
jest.config.js
- CRA で React をセットアップすると勝手に設定されますが、Next.js はそこまで親切ではないので自分で書きます
- 一応 examples にそれっぽいものはありますが、物足りないと思います
- tsconfig.json が Next.js によって勝手に書き換えられる関係で、jest 用の tsconfig を用意しています
- CRA で作ったプロジェクトみたいにテストを Watch 出来るようにしています
.scss
とかをimport
していてもコケないようにしています- 但し以下の設定では jsdom と axios の相性が良くなく、axios を使ったテストが上手く通らないケースがあります
- これは基本的にテストが SSR として実行される関係で node でないと上手く機能しないことが関係しているのではないかと考えていますが、DOM のテストには jsdom が必要なので悩ましいところ…
module.exports = { globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.jest.json', }, }, preset: 'ts-jest', clearMocks: true, setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'], roots: ['<rootDir>'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'], collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}'], testEnvironment: 'jsdom', testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|.next)[/\\\\]'], transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, watchPlugins: [ 'jest-watch-typeahead/filename', 'jest-watch-typeahead/testname', ], moduleNameMapper: { '\\.(css|less|sass|scss)$': 'identity-obj-proxy', '\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js', }, };
next-env.d.ts
.scss
とかをimport
しようとしたときにエラーにならなくなるやつです
/// <reference types="next" /> /// <reference types="next/types/global" /> /// <reference types="next/image-types/global" />
next.config.js
basePath
: SSG をビルドしたときの基準パスを設定できます- 以下の例だと
https://example.com/deploy-path-here
が基準パスになります - 開発時は不便なので
http://localhost:3000
になるようにしてます
- 以下の例だと
assetPrefix
:_next/
の位置を指定できます- 以下の例だと
https://example.com/assets-path-here/_next
が_next/
の位置になります- 既に
_next/
が存在する場所に設置するような場合に役立つと思われます
- 既に
- 開発時にはあっても意味がないので除外する設定にしています
- 以下の例だと
module.exports = (phase, { defaultConfig }) => { return { basePath: isProd ? '/deploy-path-here' : '', assetPrefix: isProd ? '/assets-path-here' : '', }; };
package.json
scripts
にこんくらい書いとくと便利かな?とは思いますdev
デバッグサーバーが起動します。ブレークポイントを設定して確認するときに使いますstart
開発サーバーが起動します。CRA で作った React と同じですbuild
out/
にプロダクションビルドを吐きますserve
out/
の中身を localhost で上げますtype-check
ESLint と TSC のチェックを走らせてエラーを検知しますtest
CRA で作った React 同様に Watch モードでテストを走らせますcoverage
テストカバレッジが出ます。但し DOM 周りのチェックはしてくれないので、ラッパーコンポーネントで Props が意図した通り刺さっているかみたいなのは計測対象外です
"scripts": { "dev": "NODE_OPTIONS='--inspect' next", "start": "next", "build": "next build --profile && next export", "serve": "serve -p 3000 ./out", "test": "jest --watch", "type-check": "tsc --noEmit && eslint .", "headless-test": "npm run type-check && jest", "coverage": "jest --coverage" },
tsconfig.jest.json
jsx
をreact-jsx
にしてるのが味噌ですnext
を蹴るとtsconfig.json
を勝手にpreserve
に書き換えてくるのでその対策です
{ "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["dom", "es2017"], "allowJs": true, "checkJs": true, "jsx": "react-jsx", "sourceMap": true, "noEmit": true, "isolatedModules": true, "strict": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "moduleResolution": "node", "baseUrl": "./", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }
tsconfig.json
- CRA で TS の React プロジェクトを生やした時のものがベースですが、より厳格に設定したような気がします
isolatedModules: true
にしておくとソースファイルにexport
やimport
が存在しないときに怒ってくれて便利ですresolveJsonModule: true
も JSON をimport
可能になるので地味に便利です
{ "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["dom", "es2017"], "allowJs": true, "checkJs": true, "jsx": "preserve", "sourceMap": true, "noEmit": true, "isolatedModules": true, "strict": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "moduleResolution": "node", "baseUrl": "./", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }
おまけ
tsconfig.json が開発サーバー起動時に書き換えられて困る
"jsx": "preserve",
にされると VSCode では入力補完がイマイチ効かなかったり、import React from 'react';
を挿入しようとしてきて面倒です- 解決策としては
next
起動後にtsconfig.json
の"jsx": "preserve",
を"jsx": "react-jsx",
に置換してあげればよいですが、next の動作に何かしらの悪影響があるかどうかまでは調べてないです- 自動化も単純なものなら concurrently とシェルスクリプトの組み合わせで簡単に実現できます
- 根本的には TypeScript の問題である可能性があるので、解決されるのを待つしかない気がします(Next.js 側で対応すべきだとは思うが…
- Next.js 側にも Issue が立ちました
今回説明した一式が欲しい人向け
:::note warn 以下のリポジトリにある内容は随時更新されるため、本記事投稿時点の内容と差異がある可能性があります :::
- Lycolia / ts-next-boilerplate に転がしてるのでどうぞ
- @lycolia/ts-boilerplate-generator-cli を使って生やすことも出来ます