GatsbyJS と TypeScript でブログを作成して公開する(3)
~ Gatsby を TypeScript 化して ESLint と Prettier を導入する ~
今回は、これまでにgatsby-starter-blog
を用いて生成されたソースを TypeScript で書けるように変更していきます。
環境構築についてはこちらを、gatsby-starter-blog についてはこちらをご覧いただければと思います。
変更履歴
- 2020/12/12: .eslintrc の設定内容を変更
- 2021/09/19: スタイルの調整
参考にさせていただいた記事
こちらの ZENN の投稿がとても参考になりました。
検証環境
- node 14.15.1
- yarn 1.22.3
- gatsby cli 2.14.0
- TypeScript 4.0.3
新しいプロジェクトの作成
まず gatsby-starter-blog を使った新しい Gatsby のプロジェクトを作成します。
以下のコマンドを叩きます。
# gatsby-typescriptという名前でプロジェクト作成
$ gatsby new gatsby-typescript https://github.com/gatsbyjs/gatsby-starter-blog
# ディレクトリを変更
$ cd gatsby-typescript
# development サーバーの立ち上げ
$ gatsby develop
この状態でhttp://localhost:8000
にアクセスすると、以下のようなページを確認できます。
ページ・コンポーネントの TypeScript 化
GatsbyJS はネイティブで TypeScript をサポートしているので、
拡張子を.ts
もしくは.tsx
にするだけで、TypeScript でページやコンポーネントを記述することができます。
また、gatsby-starter-blog にはすでに
src\pages\using-typescript.tsx
という TypeScript のページが含まれています。
src\pages\using-typescript.tsx
// If you don't want to use TypeScript you can delete this file!
import React from "react";
import { PageProps, Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
type DataProps = {
site: {
buildTime: string;
};
};
const UsingTypescript: React.FC<PageProps<DataProps>> = ({
data,
path,
location,
}) => (
<Layout title="Using TypeScript" location={location}>
<SEO title="Using TypeScript" />
<h1>Gatsby supports TypeScript by default!</h1>
<p>
This means that you can create and write <em>.ts/.tsx</em> files for your
pages, components etc. Please note that the <em>gatsby-*.js</em> files
(like gatsby-node.js) currently don't support TypeScript yet.
</p>
<p>
For type checking you'll want to install <em>typescript</em> via npm and
run <em>tsc --init</em> to create a <em>.tsconfig</em> file.
</p>
<p>
You're currently on the page "{path}" which was built on{" "}
{data.site.buildTime}.
</p>
<p>
To learn more, head over to our{" "}
<a href="https://www.gatsbyjs.com/docs/typescript/">
documentation about TypeScript
</a>
.
</p>
<Link to="/">Go back to the homepage</Link>
</Layout>
);
export default UsingTypescript;
export const query = graphql`
{
site {
buildTime(formatString: "YYYY-MM-DD hh:mm a z")
}
}
`;
こちらのページはhttp://localhost:8000/using-typescript
で確認できます。
このようにページやコンポーネントを TypeScript で書くだけなら、
特に設定を行う必要はありません。
開発環境としての TypeScript
設定なしで TypeScript が使えるのはわかりましたが、
これでは、TypeScript を使う利点があまり得られません。
using-typescript.tsx
にも書いてある通り、
タイプチェックを行うためには、typescript のインストールとtsc --init
を実行して.tsconfig
ファイルを作成する必要があります。
これだけでも良いのですが、せっかくなら Linter とか Formatter とかもカスタマイズしたいところです。
というわけで、ここでは TypeScript に加えてESLINT
とPrettier
の導入までおこない、
TypeScript を利用した開発環境として整えたいと思います。
また、今回は VSCode を使用することを前提として進めます。
TypeScript のインストール
インストール
後にインストールする ESLint のプラグインの依存関係となるため、
typescript もインストールします。
$ yarn add -D typescript
tsconfig.json の生成
次のコマンドを実行して、tsconfig.json を生成します。
yarn run tsc --init
設定内容自体は以下のように変更してください。
tsconfig.json
{
"target": "esnext",
"module": "esnext",
"lib": ["DOM", "ESNext"],
"jsx": "react",
"noEmit": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
GatsbyJS に ESLint を導入する
ESLint のセットアップを行っていきます。
ESLint のインストール
まず、eslint
とeslint-loader
をインストールします。
$ yarn add -D eslint eslint-loader
ESLint の設定ファイルの作成
ESLint の設定ファイルを作成します。
以下のコマンドを実行してください。
$ yarn run eslint --init
このコマンドを実行すると、いくつか質問が出てくるのでそれに答えていくことで、
設定ファイルを生成してくれます。
今回は以下のように答えていきます。
? How would you like to use ESLint? (Use arrow keys)
To check syntax only
> To check syntax and find problems
To check syntax, find problems, and enforce code style
? What type of modules does your project use? (Use arrow keys)
> JavaScript modules (import/export)
CommonJS (require/exports)
None of these
? Which framework does your project use? (Use arrow keys)
> React
Vue.js
None of these
? Does your project use TypeScript? (y/N) y
? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Browser
( ) Node
? What format do you want your config file to be in?
JavaScript
YAML
> JSON
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
? Would you like to install them now with npm? (Y/n) y
質問の最後に、今回の設定に必要なパッケージをインストールするか聞かれますので、これもy(es)
で答えてインストールしてもらいます。
処理がすべて終わると.eslintrc.json
というファイルが作成されます。
.eslintrc.json
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint"],
"rules": {}
}
ESLint のルールセット・プラグインの導入
次に ESLint のルールセット・プラグインを導入します。
ここでは、Airbnb が公開しているeslint-config-airbnbを導入してみます。
eslint-config-airbnb
とその依存関係をインストールします。
$ yarn add -D eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
typescript-eslint
をインストールします。
$ yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
gatsby-plugin-eslint のインストール
gatsby-plugin-eslint
という Gatsby のプラグインをインストールします。
$ yarn add -D gatsby-plugin-eslint
yarn develop
時に検査が行われるよう、以下をgatsby-config.js
に追加します。
gatsby-config.js
module.exports = {
plugins: [
/*
...省略
*/
{
resolve: "gatsby-plugin-eslint",
options: {
test: /\.js$|\.jsx$|\.ts$|\.tsx$/,
exclude: /(node_modules|.cache|public)/,
stages: ["develop"],
options: {
emitWarning: true,
failOnError: false,
},
},
},
],
};
Prettier の導入
Prettier 本体
gatsby-starter-blog
にはPrettier
もあらかじめ含まれているので、
インストールは必要ありません。
もしインストールが必要ならyarn add -D prettier
でインストール可能です。
Prettier の設定
次に Prettier の設定ファイルです。
ルートディレクトリに.prettierrc
というファイルがありますので、
そちらを編集すれば OK です!
例えば以下のように設定してみます。
.prettierrc
{
"arrowParens": "avoid",
"semi": true,
"endOfLine": "lf",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
このあたりのルールはプロジェクトによりけりでしょうから、
こちらを見て必要なものを設定していってください。
.prettierignore の編集
Prettier によるフォーマットを避けたいファイルやディレクトリは.prettierignore
に設定します。
すでに用意されているファイルですが、以下は追加しておくとよいでしょう
tsconfig.json
.eslintrc.json
ESLint と Prettier の競合を解決する
今回 Linter として ESLint を Code Formatter として Prettier を用いるわけですが、
そのままだと、両者のルールは競合してしまいます。
これらはいちいち設定を探して回避もできますが、競合ルールを簡単に解決する方法があるのでこちらを利用します。
eslint-config-prettier
をインストールします。
$ yarn add -D eslint-config-prettier
ESLint の設定を編集
ESLint 用のプラグインと Prettier をインストールしたら
ESLint の設定を編集します。
.eslintrc.json
ファイルを開いて、以下のように編集してください。
{
/*
...省略
*/
"extends": [
"airbnb",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"prettier",
"prettier/@typescript-eslint",
"prettier/react
],
"parser": "@typescript-eslint/parser",
"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
},
/*
...省略
*/
"rules": {
"no-use-before-define": "off",
"quotes": [2, "single", { "avoidEscape": true }],
"react/jsx-filename-extension": [
"error",
{ "extensions": [".jsx", ".tsx"] }
],
"react/prop-types": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-var-requires": "off",
"react/no-danger": "off",
"react/no-unescaped-entities": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
]
}
}
ESLint
Prettier
の拡張機能を利用する
VSCode の次に VSCode に ESLint と Prettier の拡張機能をインストールします。
これらを入れることで、リアルタイムの Linting と、
ファイル保存時の Format を可能にします。
インストール
VSCode の拡張機能の検索でそれぞれすぐに見つかると思いますのでインストールしてください。
VSCode の設定
VSCode をひらき、Ctrl + Shift + P
(Mac はCmd + Shift + P
)でコマンドパレットを表示し、
workspace settings json
と入力して Enter を押してください。
setting.json
ファイルが開きますので、これに以下を記述して保存して下さい。
{
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.eol": "\n"
}
ESLint と gatsby-plugin-eslint の確認
ここで一度設定の確認をします。
まず、起動している場合は一度開発サーバーを止め、
gatsby develop
を実行しなおします。
(うまく実行できない場合は一度npm install
を実行してみてください)
すると以下のようなエラーが大量に表示されるはずです。
warning ESLintError:
C:\Users\Kohsuk\Documents\Projects\gatsby\gatsby-typescript\gatsby-browser.js
2:8 error Strings must use singlequote quotes
3:8 error Strings must use singlequote quotes
5:8 error Strings must use singlequote quotes
7:8 error Strings must use singlequote quotes
10:8 error Strings must use singlequote quotes
✖ 5 problems (5 errors, 0 warnings)
5 errors and 0 warnings potentially fixable with the
`--fix` option.
gatasby-starter-blog
のコードが今回設定した ESLint のルールに違反しているためです。
ESLint のエラーが出ていてもhttp://localhost:8000
でページの確認自体は可能です。
エラーを解消する
以上で ESLint, Prettier 周りの設定は完了しました。
今度はエラーを修正していきます。
フォーマット用のスクリプトを作成
まず、フォーマットのためのスクリプトを用意します。
package.json
にデフォルトでformat
が用意されており、prettier でフォーマットできるようになっています。
ここで ESLint も実行できるようにしておき、ESLint のエラーも一緒に解消したいと思います。
"scripts": {
"format:prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"format:eslint": "eslint --fix \"**/*.{js,jsx,ts,tsx}\"",
"format": "yarn format:eslint && yarn format:prettier",
}
今回は、ESLint -> Prettier の順で実行するスクリプトを書きました。
eslint-plugin-prettier
というプラグインを用いれば、
ESlint 内で prettier を実行することもできますが、
こちらの記事を参考にさせていただき、
今回はこのような方法を取っています。
format スクリプトを実行
では上で設定したスクリプトを実行して、エラーを解消します。
$ yarn format
取り切れないエラー
スクリプトの実行でフォーマットがおこなわれるので、
大方のエラーは解消されるはずです。
ただ、VSCode 上で見ると、ESLint Typescript のエラーがいくつか取り切れていないかと思います。
src/components/layout.js
の 5 行目の__PATH_PREFIX__
1. .eslintrc.json
に以下を追加すれば解決します。
`.eslintrc.json
{
"globals": {
"__PATH_PREFIX__": true
}
}
.js
ファイルに Jsx がある
2. .jsx
または.tsx
以外に Jsx を書くとエラーになるように設定してあります。
これらのファイルは後で TypeScript に書き換え、.tsx
に変更するので問題ないですが、気になる場合はいったん拡張子を.jsx
に変更します。
注意点として、src/templates/blog-post.js
の拡張子を変更した場合は、このパスを参照しているgatsby-node.js
8 行目の拡張子もjsx
に変更する必要があります。
src/components/seo.js
の 9 行目prop-types
3. TypeScript に書き換える場合は、props-types でタイプチェックする必要がないので、
無視しておいてかまわないかと思います。
JavaScript のままにしておきたい場合は、エラーメッセージの通り、prop-types
をインストールしてください(yarn add prop-types
)。
src/pages/using-typescript.tsx
の 5, 6 行目
4. js ファイルから import しているため、型定義ファイルがなくエラーになっています。
layout.js
, seo.js
ともに TypeScript に書き換えれば問題ないので無視してかまわないと思います。
ここで再度gatsby develop
コマンドを実行すると、prop-types
のエラー以外は消えているはずです。
TypeScript に書き換える
ようやく各ファイルの TypeScript 化を進めていきます。
まず、src/pages/index.js
の拡張子を.tsx
に変更してみます。
すると BlogIndex コンポーネントの引数に型の指定がないためエラーとなります。
まず以下のように変更します(変更
というコメントのある行)。
src/pages/index.tsx
import React from "react";
import { Link, graphql, PageProps } from "gatsby"; // <- 変更
import Bio from "../components/bio";
import Layout from "../components/layout";
import SEO from "../components/seo";
const BlogIndex: React.FC<PageProps> = ({ data, location }) => {
// <- 変更
const siteTitle = data.site.siteMetadata?.title || "Title"; // <- ここはエラーが残っている
const posts = data.allMarkdownRemark.nodes; // <- ここはエラーが残っている
/*
...省略
*/
};
export default BlogIndex;
export const pageQuery = graphql`
query BlogIndex { // <- 変更
site {
siteMetadata {
title
}
}
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
nodes {
excerpt
fields {
slug
}
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
description
}
}
}
}
`;
これで, Props の型に関するエラーは解消されたはずです。
しかしまだ、ここはエラーが残っている
とコメントを書いた部分にエラーが出ています。
こちらは、GraphQL のクエリのレスポンスを参照している部分で、
その定義がないためエラーとなっています。
GraphQL のレスポンスの型を生成する
GraphQL のレスポンスの型は自動生成することができます。
今回はgatsby-plugin-typegen
というものを使用します。
gatsby-plugin-typegen をインストール
まずはプラグインをインストールします。
$ yarn add gatsby-plugin-typegen
つぎにこのプラグインを読み込めるようにgatsby-config.js
に追記します。
gatsby-config.js
module.exports = {
/*
...省略
*/
plugins: [
/*
...省略
*/
'gatsby-plugin-typegen',
]
ここまで出来たら、gatsby develop
を実行しなおします。
すると、src/__generated__/gatsby-types.ts
が自動生成されます。
このファイルに以下のような定義が追加されているはずです。
src/__generated__/gatsby-types.ts
type BlogIndexQuery = {
readonly site: Maybe<{
readonly siteMetadata: Maybe<Pick<SiteSiteMetadata, "title">>;
}>;
readonly allMarkdownRemark: {
readonly nodes: ReadonlyArray<
Pick<MarkdownRemark, "excerpt"> & {
readonly fields: Maybe<Pick<Fields, "slug">>;
readonly frontmatter: Maybe<
Pick<Frontmatter, "date" | "title" | "description">
>;
}
>;
};
};
BlogIndexQuery
という名前は、先ほどsrc/pages/index.tsx
のクエリの定義部分に追加した
BlogIndex
というクエリの名前にQuery
がついたもので、
このルールで生成されるようです。
生成されたクエリの型を使用する
ではこれを使ってみます。
再びsrc/pages/index.tsx
を開き以下のように編集してください。
const BlogIndex: React.FC<PageProps<GatsbyTypes.BlogIndexQuery>> = ({ // <- 変更
data,
location,
}) => {
このようにGatsbyTypes
で生成された型を参照できます。
あとはオプショナルチェイニング演算子?.
や三項演算子などを使えばエラーを解決できるはずです。
最終的にはこのようになりました。
// src/__generated__/gatsby-types.ts
import React from 'react';
import { Link, graphql, PageProps } from 'gatsby';
import Bio from '../components/bio';
import Layout from '../components/layout';
import SEO from '../components/seo';
const BlogIndex: React.FC<PageProps<GatsbyTypes.BlogIndexQuery>> = ({
data,
location,
}) => {
const siteTitle = data.site?.siteMetadata?.title || 'Title';
const posts = data.allMarkdownRemark.nodes;
if (posts.length === 0) {
return (
そいstjjt location={location} title={siteTitle}>
<SEO title="All posts" />
<Bio />
<p>
No blog posts found. Add markdown posts to "content/blog" (or the
directory you specified for the "gatsby-source-filesystem" plugin in
gatsby-config.js).
</p>
</Layout>
);
}
return (
<Layout location={location} title={siteTitle}>
<SEO title="All posts" />
<Bio />
<ol style={{ listStyle: 'none' }}>
{posts.map(post => {
const title = post.frontmatter?.title || post.fields?.slug;
return (
<li key={post.fields?.slug}>
<article
className="post-list-item"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h2>
<Link to={post.fields?.slug || '/'} itemProp="url">
<span itemProp="headline">{title}</span>
</Link>
</h2>
<small>{post.frontmatter?.date}</small>
</header>
<section>
<p
dangerouslySetInnerHTML={{
__html:
post.frontmatter?.description || post.excerpt || '',
}}
itemProp="description"
/>
</section>
</article>
</li>
);
})}
</ol>
</Layout>
);
};
export default BlogIndex;
export const pageQuery = graphql`
query BlogIndex {
site {
siteMetadata {
title
}
}
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
nodes {
excerpt
fields {
slug
}
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
description
}
}
}
}
`;
他のコンポーネントもこの方法で TypeScript に置き換えていくことができます!
gatsby-node.js の TypeScript 化
あとはgatsby-node.js
を TypeScript 化します。
まずはts-node
をインストールします。
$ yarn add -D ts-node
次に、src/gatsby-node/index.ts
ファイルを作成し、gatsby-node.js
を丸ごとコピーして貼り付けます。
コンポーネントの時と同じように、エラーがなくなるまで修正します。
少々苦労しましたが、最終的にこれでエラーがなくなります。
src/gatsby-node/index.ts
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import path from "path";
import { createFilePath } from "gatsby-source-filesystem";
import { GatsbyNode, Actions } from "gatsby";
export const createPages: GatsbyNode["createPages"] = async ({
graphql,
actions,
reporter,
}) => {
const { createPage } = actions;
// Define a template for blog post
const blogPost = path.resolve("./src/templates/blog-post.jsx");
// Get all markdown blog posts sorted by date
const result = await graphql<{
allMarkdownRemark: Pick<GatsbyTypes.Query["allMarkdownRemark"], "nodes">;
}>(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___date], order: ASC }
limit: 1000
) {
nodes {
id
fields {
slug
}
}
}
}
`
);
if (result.errors) {
reporter.panicOnBuild(
"There was an error loading your blog posts",
result.errors
);
return;
}
const posts = result.data!.allMarkdownRemark.nodes;
// Create blog posts pages
// But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
// `context` is available in the template as a prop and as a variable in GraphQL
if (posts && posts.length > 0) {
posts.forEach((post, index) => {
const previousPostId = index === 0 ? null : posts[index - 1].id;
const nextPostId =
index === posts.length - 1 ? null : posts[index + 1].id;
createPage({
path: post.fields!.slug || "/",
component: blogPost,
context: {
id: post.id,
previousPostId,
nextPostId,
},
});
});
}
};
export const onCreateNode: GatsbyNode["onCreateNode"] = ({
node,
actions,
getNode,
}) => {
const { createNodeField } = actions;
if (node.internal.type === "MarkdownRemark") {
const value = createFilePath({ node, getNode });
createNodeField({
name: "slug",
node,
value,
});
}
};
export const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] =
async ({ actions }: { actions: Actions }) => {
const { createTypes } = actions;
// Explicitly define the siteMetadata {} object
// This way those will always be defined even if removed from gatsby-config.js
// Also explicitly define the Markdown frontmatter
// This way the "MarkdownRemark" queries will return `null` even when no
// blog posts are stored inside "content/blog" instead of returning an error
createTypes(`
type SiteSiteMetadata {
author: Author
siteUrl: String
social: Social
}
type Author {
name: String
summary: String
}
type Social {
twitter: String
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
fields: Fields
}
type Frontmatter {
title: String
description: String
date: Date @dateformat
}
type Fields {
slug: String
}
`);
};
次に、もともとあったgatsby-node.js
は上のファイルを参照するように変更します。
gatsby-node.js
/* eslint-disable */
"use strict";
require("ts-node").register({
compilerOptions: {
module: "commonjs",
target: "esnext",
},
});
require("./src/__generated__/gatsby-types");
const {
createPages,
onCreateNode,
createSchemaCustomization,
} = require("./src/gatsby-node/index");
exports.createPages = createPages;
exports.onCreateNode = onCreateNode;
exports.createSchemaCustomization = createSchemaCustomization;
gatsby-node.js
とsrc/gatsby-node/index.ts
ではうまくエラーが取れない部分があったので、
一部 ESLint を無効にしています。
まとめ
以上で終わりです!
かなり長くなってしまいましたが、参考になれば幸いです。
それではまた。