お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
言語::TypeScriptライブラリ::Next.js

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 の導入

  1. 次のコマンドを流し必要なものを一式入れる
    1. バージョンの整合性が上手く合わないときは気合で調整する
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

import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';

jest.config.js

  • CRAでReactをセットアップすると勝手に設定されますが、Next.jsはそこまで親切ではないので自分で書きます
  • 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

  • jsxreact-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にしておくとソースファイルにexportimportが存在しないときに怒ってくれて便利です
  • 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が立ちました

今回説明した一式が欲しい人向け

以下のリポジトリにある内容は随時更新されるため、本記事投稿時点の内容と差異がある可能性があります

投稿日:
ソフトウェア::zsh

確認環境

Env Ver
zsh zsh 5.8 (x86_64-pc-msys)

サンプルコード

例として.zshrcに書くものとする(別に分割しても構わない)

  1. setopt PROMPT_SUBSTする
    1. この設定によりパラメータ展開、コマンド置換、および算術展開がプロンプトで実行される
  2. 展開したい関数をシングルクォートで囲み文字列結合する
    1. 注意点
      1. 変数に代入している場合評価されない
      2. 関数はシングルクォートで囲まないと評価されない
# baz は echo が入った関数
PROMPT "foobar"'$(baz)'
# 以下の 2 つは期待通り動作しない
PROMPT "foobar"$(baz)
PROMPT $(baz)
投稿日:
Node.js::その他

Babelとtscどっちがいいのか気になったので調べて見たメモ
結論から言うと基本的にはtscで良い

確認環境

Env Ver
@babel/cli 7.14.5
@babel/core 7.14.6
@babel/preset-env 7.14.7
@babel/preset-typescript 7.14.5
typescript 4.3.2

Babelとは何か?

What is Babel? よりBabelとはJavaScriptのコンパイラと説明されている

Babelがしてくれること

  • 構文の変換
  • corejsを使ったPolyfill
  • ソースコードの変換(codemods
  • その他色々

基本的にはES6+をES6にしてくれると考えれば良さそうです
でもそれって別にtscでもいいいよねって思う

Babelの導入

基本はこれ

npm i -D @babel/core @babel/cli @babel/preset-env

Babelの設定

.babelrcを作ってその中にJSONを書いていく
こんな感じ

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "ie": "11",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Babelの機能について

コンポーネント

@babel/cli

BabelのCLI、これがないと始まらない

@babel/core

Babel本体、CLIがよしなにしてくれる

@babel/preset-env

構文の変換やPolyfillを設定できる

@babel/preset-typescript

TSをトランスパイルしてくれる

BabelとBrowserlist

BabelはBrowserlistの設定を認識して自動でPolyfillを挿入してくれます
なお、Babel 7.4.0以前では設定方法が異なる可能性があります

サンプルコード

IE11をターゲットにした設定のサンプルです
以下のコマンドを流せばPolyfillされたJSが出ることを確認できます
npx babel src -d dest --extensions ".ts"

.babelrc
{
  "presets": [
    ["@babel/preset-env", { "corejs": 3, "useBuiltIns": "usage" }],
    ["@babel/preset-typescript"]
  ]
}
.browserlist
ie 11

Babelとtscでトランスパイルしてみる

割と違うコードが出てきます

元のソース

const sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
const prm = new Promise((res) => res(true));
console.log(prm);
[...Array(10)].forEach((_, i) => console.log(i));

console.log(globalThis.Date());

export {};

Babel

.babelrc
{
  "presets": [["@babel/preset-env"], ["@babel/preset-typescript"]]
}
ビルド結果
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true,
});

function _toConsumableArray(arr) {
    return (
        _arrayWithoutHoles(arr) ||
        _iterableToArray(arr) ||
        _unsupportedIterableToArray(arr) ||
        _nonIterableSpread()
    );
}

function _nonIterableSpread() {
    throw new TypeError(
        "Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
    );
}

function _unsupportedIterableToArray(o, minLen) {
    if (!o) return;
    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
    var n = Object.prototype.toString.call(o).slice(8, -1);
    if (n === "Object" && o.constructor) n = o.constructor.name;
    if (n === "Map" || n === "Set") return Array.from(o);
    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
        return _arrayLikeToArray(o, minLen);
}

function _iterableToArray(iter) {
    if (
        (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) ||
        iter["@@iterator"] != null
    )
        return Array.from(iter);
}

function _arrayWithoutHoles(arr) {
    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}

function _arrayLikeToArray(arr, len) {
    if (len == null || len > arr.length) len = arr.length;
    for (var i = 0, arr2 = new Array(len); i < len; i++) {
        arr2[i] = arr[i];
    }
    return arr2;
}

var sp = new URLSearchParams("?aaa=bbb&ccc");
console.log(sp);
var prm = new Promise(function (res) {
    return res(true);
});
console.log(prm);

_toConsumableArray(Array(10)).forEach(function (_, i) {
    return console.log(i);
});

console.log(globalThis.Date());

tsc

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": true,
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "isolatedModules": true,
    "baseUrl": ".",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}
ビルド結果
"use strict";
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
    for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
        to[j] = from[i];
    return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
var sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
var prm = new Promise(function (res) { return res(true); });
console.log(prm);
__spreadArray([], Array(10)).forEach(function (_, i) { return console.log(i); });
console.log(globalThis.Date());
//# sourceMappingURL=index.js.map

結論

tscで問題ない

tscの方が綺麗なコードが出てきてるので、Polyfill要らなければtscで問題ないです
因みにES6+をES5にするのもできるのでJSのトランスパイルにも使えます

core-jsを使うならBabel

ちょいちょい触っててBabelの利点はcore-jsがあれば.browserlistを使えるので、そこでターゲットを指定してやればPolyfillを勝手に差し込んでくれるところですね

但しCRAではBabelに対する.browserlistはほぼ無価値

あとはCreate React AppはビルドにBabelを採用しているので、FW側でビルドパイプラインがあるときには採用したほうが楽です
(態々書き換える意味もないので)
ただreact-scripts 4.0.3にはcore-jsが入っていないので、基本的に.browserlistを書いたところでPolyfillは入らないため、あんまり存在感はないです

投稿日:
Node.js::webpack

バンドル内容をHTMLに注入するの続き

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'
     })
  ]
};