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"
}
]
}
}
VSCodeのESLint
Prettier
の拡張機能を利用する
次に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のエラーがいくつか取り切れていないかと思います。
1. src/components/layout.js
の5行目の__PATH_PREFIX__
.eslintrc.json
に以下を追加すれば解決します。
`.eslintrc.json
{
"globals": {
"__PATH_PREFIX__": true
}
}
2. .js
ファイルにJsxがある
.jsx
または.tsx
以外にJsxを書くとエラーになるように設定してあります。
これらのファイルは後でTypeScriptに書き換え、.tsx
に変更するので問題ないですが、気になる場合はいったん拡張子を.jsx
に変更します。
注意点として、src/templates/blog-post.js
の拡張子を変更した場合は、このパスを参照しているgatsby-node.js
8行目の拡張子もjsx
に変更する必要があります。
3. src/components/seo.js
の9行目prop-types
TypeScriptに書き換える場合は、props-typesでタイプチェックする必要がないので、
無視しておいてかまわないかと思います。
JavaScriptのままにしておきたい場合は、エラーメッセージの通り、prop-types
をインストールしてください(yarn add prop-types
)。
4. src/pages/using-typescript.tsx
の5, 6行目
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を無効にしています。
まとめ
以上で終わりです!
かなり長くなってしまいましたが、参考になれば幸いです。
それではまた。