公式のドキュメントには書かれていない気がするがDockerだけあればセットアップ可能。WSL2でのセットアップが面倒なVargantは不要
本記事ではWSL2内のUbuntuの中でDockerとDocker Composeを使った構築法について記述する。Windows向けのDocker Desktopは使用しない
前提
- WSL2のUbuntuにVSCodeで接続して作業
 - 起動してログイン、トゥート、設定と言った最低限の画面操作が可能な程度まで
 
確認環境
| Env | ver | 
|---|---|
| Mastodon | v3.5.3 | 
| Docker | 20.10.21, build baeda1f | 
| Docker Compose | v2.12.2 | 
| Ubuntu | 20.04.5 LTS | 
| WSL2 | 1.0.3.0 | 
| DevContainer | v0.266.1 | 
| VSCode | 1.73.0 | 
手順
git clone https://github.com/mastodon/mastodon.git- 最新版などバージョン指定が必要な場合は追加で
tagをチェックアウトする 
- 最新版などバージョン指定が必要な場合は追加で
 - VSCodeでClone先を開く
 - Reopen Containerする
 - セットアップが走るので暫く待つ
 foreman startを流す- http://localhost:3000/ へアクセス
 
操作方法
ユーザーを作ったりしたい場合、Using the admin CLIが参考になる
一例としてユーザー作成は次の書式で行ける
./bin/tootctl accounts create foo --email foo@example.com --confirmed
トラブルシュート
DB接続エラーが出る
ActiveRecord::ConnectionNotEstablished at /
could not connect to server: No such file or directory
Is the server running locally and accepting
connections on Unix domain socket "/var/run/postgresql//.s.PGSQL.5432"?
上記のエラーが出る場合、ビルドしたDockerイメージが壊れてるので以下のコマンドで全て消して作り直すとうまく行く
docker rm -f `docker ps -a -q`
docker rmi `docker images -q`
docker system prune
他のMastodonインスタンスが動かない
複数のMastodonインスタンスのDocker環境を同居させると多分コンフリクトする
ローカル環境用の開発ドメインをhttps化する時に使えるやつ
例えばローカル環境に複数のサービスがいて、それぞれをhttps://*.example.com/のようなドメインで管理したい時に使える
確認環境
同じことをすればLinuxとかでも応用できると思う
| Env | Ver | 
|---|---|
| nginx | 1.19.8 | 
| mkcert | 1.4.3 | 
| Windows 10 Pro | 19043.1415 | 
手順
- mkcert の導入とワイルドカード証明書の作成
choco install mkcert # mkcertを認証局として登録 mkcert -install # 証明書を作成するドメインを列挙 mkcert example.test *.example.test mv _wildcard.example.com+1.* C:/nginx/conf/.ssl/ nginxの設定に証明書を記載
server { server_name dev.example.com; listen 443 ssl; ssl_certificate ssl/_wildcard.example.com+1.pem; ssl_certificate_key ssl/_wildcard.example.com+1-key.pem; ... }
別にReactに限った話でもないのですが、実務で悩ましいコードにあたって頭を抱えてる内にハゲてきたので、ハゲがこれ以上進まない事を祈り書きました。いやいっそのこと全ハゲになりたいですが、それはさておき
とりあえず個人的には以下2点を意識すれば起きないとは思うので、Next.jsを使った簡単なサンプルを交えながら3例ほどケーススタディ形式で紹介していきます
- 密結合にしない
 - シンプルに実装する
 
挙げている事例については例示用にフルスクラッチで書いています(主題と関係ないコードは端折ってます
ここはこうした方がより良いのでは?などのご意見があれば是非コメントいただけると嬉しいです
コンポーネントのOptional引数のオーバーライド
コンポーネント引数をコンポーネント側で書き換えるような実装はどうかなと思います
問題点
- 親の預かり知らぬところで値が設定されている
- 誰かが知らずに設定値をすり替えたりしたら不具合が起きそうです
 
 - 型のコメントと設定値が異なる
- 型のコメントと実装が異なるので、誰かが直すかもしれません
- すると、コンポーネントを参照している全コンポーネントに影響が波及します
 
 - そもそも型と実装は本質的に関係ないので、こういった運用はNG
 
 - 型のコメントと実装が異なるので、誰かが直すかもしれません
 
一例
type AccountContainerProps = {
  email: string;
  // デフォルトtrue
  isUpdate?: boolean;
  // デフォルトfalse
  gotNotify?: boolean;
};
export const AccountContainer = ({
  isUpdate = true,
  gotNotify = true,
  ...props
}: AccountContainerProps) => {
  ...
}
改善案
呼び元で明示的に指定するように変更しています
これによって親は子の実装を知る必要がなくなり、責務がそこで閉じるようになりました
改善点
- 親の預かり知らぬところで値が設定されなくなった
- これで子コンポーネントで値が変わることに怯える必要はなくなりました
 
 - 型の初期値コメントを削除できた
- 実装と一致する保証がないコメントは削除するべきでしょう
 
 
type AccountContainerProps = {
  email: string;
  isUpdate: boolean;
  gotNotify: boolean;
};
export const AccountContainer = (props: AccountContainerProps) => {
  ...
}
コンポーネントのOptional引数の多重オーバーライド
コンポーネントのOptional引数のオーバーライドが多重化されている上に、なんか途中で更に書き換えられているとかいう地獄
どうしてそんなことをするのか…
一例
type AccountTemplateProps = {
  email: string;
  // デフォルトtrue
  isUpdate?: boolean;
  // デフォルトfalse
  gotNotify?: boolean;
  from: 'register' | 'update';
};
export const AccountTemplate = ({
  isUpdate = false,
  gotNotify = true,
  ...props
}: AccountTemplateProps) => {
  if (props.from === 'register') isUpdate = false;
  return <AccountContainer {...props} />;
};
type AccountContainerProps = {
  email: string;
  // デフォルトtrue
  isUpdate?: boolean;
  // デフォルトfalse
  gotNotify?: boolean;
};
export const AccountContainer = ({
  isUpdate = true,
  gotNotify = true,
  ...props
}: AccountContainerProps) => {
  ...
}
改善案
前項のように明示的に値を渡してあげるようにしましょう
直列に分散されたコンポーネント
コンポーネントの中にコンポーネントがネストされ続けてるパターンです
一見して何をしているのか分かりづらい上、StateやらHookやら色んな処理が各コンポーネントに分散配置されていることもあります
問題点
- 親からみると子が何をしているのかが分かりづらい
 - コンポーネント名が意味をなしていない(責務が別れていない)
 - 子コンポーネントが状態を持っているため、親コンポーネントでハンドリングができない
 - 子コンポーネントのロジック変更が参照している全コンポーネントのロジックに波及する
 
一例
親コンポーネント
RegisterHeaderなる物が差し込まれていることだけがわかります
このコンポーネントが何をするのかはパッと見ではよくわかりません
const AccountUpdatePage = () => {
  return <Register header={<RegisterHeader />} />;
};
ラッピングしているコンポーネント
ラッパーなのでこのコンポーネントそのものは何をしているのかわかりません
type RegisterProps = {
  header: JSX.Element;
};
export const Register = (props: RegisterProps) => {
  return <>{props.header}</>;
};
差し込まれているコンポーネントの中身
どうやらRegisterHeaderはRegisterContentを含むようです
なんでヘッダーの中にコンテンツがあるのでしょうか…
export const RegisterHeader = () => {
  return (
    <>
      <p>head</p>
      <RegisterContent />
    </>
  );
};
差し込まれているコンポーネントの子
現在のパスに応じて叩くAPIを変えるような実装がされていますが、もしパスが変わったり増えたりしたらこのコンポーネントの実装を知らない限り面倒なことになります
export const RegisterContent = () => {
  const rt = useRouter();
  const currentPath = rt.pathname;
  const url =
    currentPath === 'register'
      ? 'https://example.com/kaiin/touroku'
      : 'https://example.com/kaiin/koshin';
  const [username, setUsername] = useState<string | undefined>(undefined);
  const onSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
    ev.preventDefault();
    axios.post(url, {
      username,
    });
  };
  return (
    <>
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={username}
          onChange={(ev) => setUsername(ev.target.value)}
        />
        <button>送信</button>
      </form>
      <RegisterFooter />
    </>
  );
};
export const RegisterFooter = () => {
  return <p>foot</p>;
};
改善案
改善点
- 親から子への見通しが改善された
 - コンポーネント名が名前の通りの意味を持つようになった
- ヘッダーはヘッダー、フォームはフォーム、フッターはフッターの責務だけに集中できます
 Registerとか言う謎コンポーネントも姿を消しました
 - 状態をすべて親に集約した
 - 子コンポーネントのロジック変更が起きる確率が減った
- 子コンポーネント側のロジックを減らし、親から渡されるコールバックで行うようにしたため、子コンポーネントのロジック変更が他に影響する確率が減りました
 
 
親コンポーネント
- ひとまずヘッダーと入力フォーム、フッターがあるんだなという事が解るようにはなったと思います
 - 状態を親に集約したのでAPI叩く時のURIも態々パスから判断しなくて良くなったのでコードの複雑性が減っています
 
const usePageState = () => {
  const [username, setUsername] = useState('');
  return {
    username,
    setUsername,
  };
};
const onSubmit = (username: string) => {
  axios.post('https://example.com/kaiin/koshin', {
    username,
  });
};
const AccountUpdatePage = () => {
  const ps = usePageState();
  return (
    <>
      <RegisterHeader />
      <RegisterForm
        onChangeUsername={ps.setUsername}
        username={ps.username}
        onSubmit={() => onSubmit(ps.username)}
      />
      <RegisterFooter />
    </>
  );
};
ヘッダー
export const RegisterHeader = () => {
  return <p>head</p>;
};
入力フォーム
type RegisterFormProps = {
  username: string;
  onChangeUsername: (value: string) => void;
  onSubmit: () => void;
};
const onChangeUsername = (
  ev: React.ChangeEvent<HTMLInputElement>,
  setState: (value: string) => void
) => {
  const value = ev.target.value;
  setState(value);
};
const onSubmit = (
  ev: React.FormEvent<HTMLFormElement>,
  onSubmit: () => void
) => {
  ev.preventDefault();
  onSubmit();
};
export const RegisterForm = (props: RegisterFormProps) => {
  return (
    <>
      <form onSubmit={(ev) => onSubmit(ev, props.onSubmit)}>
        <input
          type="text"
          value={props.username}
          onChange={(ev) => onChangeUsername(ev, props.onChangeUsername)}
        />
        <button>送信</button>
      </form>
    </>
  );
};
フッター
export const RegisterFooter = () => {
  return <p>foot</p>;
};
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と同じですbuildout/にプロダクションビルドを吐きますserveout/の中身をlocalhostで上げますtype-checkESLintとTSCのチェックを走らせてエラーを検知しますtestCRAで作った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が立ちました
 
今回説明した一式が欲しい人向け
以下のリポジトリにある内容は随時更新されるため、本記事投稿時点の内容と差異がある可能性があります
- Lycolia / ts-next-boilerplate に転がしてるのでどうぞ
 - @lycolia/ts-boilerplate-generator-cli を使って生やすことも出来ます
 
Webpackには次の2つの開発モードがあるdevelopment, production
開発モードの設定
modeプロパティに'development', 'production', 'none'を設定する
これを設定するとprocess.env.NODE_ENVで値を取得できる様になる
それぞれの違いは mode-development にあるが、試してみた感じproductionは出力が最適化され、console系が消えるものと思われる
noneを設定するとminifyとかがされてない生のコードが出てくる
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
+  mode: 'development', 
  entry: {
    index: './src/index.js',
    print: './src/print.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  plugins: [
     new HtmlWebpackPlugin({
       title: 'Webpack',
       template: 'index.html'
     })
  ]
};