jackmiwamiwa devblog

フロントエンドをメインに行ったことをまとめていくサイトになります。

「swiper/react」をちょっとだけ使いやすくする

「swiper/react」をさわった時に少し触りやすいようにしたので、その時のコードです。

swiperjs.com

結果

使っているバージョン

  • "react": "^18.2.0",
  • "swiper": "^9.2.0",

コンポーネント

import type { FC, ReactNode } from 'react';
import { Children } from 'react';
import 'swiper/css';
import { Swiper, SwiperSlide } from 'swiper/react';
import styles from './index.module.scss';

export type SliderProps = {
  children: ReactNode;
  slidesPerView: number;
  spaceBetween: number;
  loop?: boolean;
  visible?: 'visible';
};

export const Slider: FC<SliderProps> = ({ children, slidesPerView, spaceBetween, loop, visible }) => {
  return (
    <div className={styles['slider']} data-visible={visible}>
      <Swiper
        slidesPerView={slidesPerView}
        spaceBetween={spaceBetween}
        speed={500}
        className={styles['slider__swiper']}
        loop={loop}
      >
        {Children.map(children, (slide, index) => (
          <SwiperSlide key={index}>{slide}</SwiperSlide>
        ))}
      </Swiper>
    </div>
  );
};
$root :'.slider';

.slider {
  // 何かのスタイル
}

.slider__swiper {
  #{$root}[data-modifier='visible'] & {
    overflow: visible !important;
  }
}

ページ側

<Slider slidesPerView={3} spaceBetween={96}>
  <p style={{ border: '1px solid #000' }}>テキスト1</p>
  <p style={{ border: '1px solid #000' }}>テキスト2</p>
  <p style={{ border: '1px solid #000' }}>テキスト3</p>
  <p style={{ border: '1px solid #000' }}>テキスト4</p>
  <p style={{ border: '1px solid #000' }}>テキスト5</p>
</Slider>

TypeScirptで配列・オブジェクトの値をEnums化して型を安全にする

以下の記事を参考にTypeScriptでの配列の値を記載する際にEnums化を行ったため、その時のメモになります。

blog.mitsuruog.info

行いたかったこと

react-hook-formで以下のようなことをする際にテキストだと型がうまく反応しなかったため、反応するようにしたい

const inputNameList = {
 group: ['input1', 'input2']
}
<!-- 以下の「input1」の部分を「inputNameList」以外の名前の場合にエラーにしたい -->
<input {...register("input1")} />

<!-- OKパターン(inputNameListの中にinput1はあるので、OK) -->
<input {...register("input1")} />
<!-- NGパターン(inputNameListの中にinput3はないので、NG) -->
<input {...register("input3")} />

今回実装したコード

オブジェクトのkey部分を使用したい場合

一覧の定義・使い方

// 定義の内容
const inputNameList = {
  group1: ['data1', 'data2', 'data3'],
};
// 使い方
/**
 * inputNameList.group1 // 'group1'のテキスト
 */

関数の定義

/**
 * Function to create an enum based on an array of names
 * @template { string[] } T Type of list of names in object first level
 * @template { string } S  List name type of object first level names
 * @param { T } keys List of names in object first level
 */
export function convertArrayToEnum<T extends unknown[], S extends string>(keys: T) {
  const result = {} as { [key in S]: S };
  keys.forEach((k: S) => {
    result[k] = k;
  });
  return result;
}

実際の使い方

// 一覧の定義
const inputNameList = {
  group1: ['data1', 'data2', 'data3'],
  group2: ['test1'],
};
// オブジェクトのkey部分を配列にて、取得
// 結果: ['group1', 'group2']
const objectList = Object.keys(inputNameList) as (keyof typeof inputNameList)[];
// 各種型の定義
// 結果: ('group1' | 'group2')[]
type InputGroupKeys = typeof validateGroupKeys;
// 結果: 'group1' | 'group2'
type InputGroupKeysNumber = typeof validateGroupKeys[number]
// enum化
/** 結果
 * const enum = {
 *    group1: "group1";
 *    group2: "group1";
 * }
 */
const enum = convertArrayToEnum<InputGroupKeys, InputGroupKeysNumber>(objectList);
// 実際に使用
enum.group1 //「group1」のテキスト

オブジェクトの配列部分を取得したい場合

一覧の定義・使い方

// 定義の内容
const inputNameList = {
  group1: ['data1', 'data2', 'data3'],
};
// 使い方
/**
 * group1.data1 // 'data1'のテキスト
 * group1.data2 // 'data2'のテキスト
 * group1.data3 // 'data3'のテキスト
 */

関数の定義

/**
 * Function to create an enum based on the name of an array in a property
 * @template { string[] } T Type of list of names in object first level
 * @template { string } S List name type of object first level names
 * @template { Object<string> } K  List name type of object first level names
 * @param { Object<S, string[]> } group group list
 * @param { T } keys List of names in object first level
 */
export function convertObjectArrayToEnum<T extends unknown[], S extends string, K extends object>(
  group: { [key in S]: string[] },
  keys: T
) {
  type InputLabel = { [key in keyof K]: string };
  const result = {} as { [key in S]: InputLabel };
  keys.forEach((k: S) => {
    result[k] = {} as InputLabel;
    group[k].forEach((value: string) => {
      result[k][value] = value;
    });
  });
  return result;
}

実際の使い方

// 一覧の定義
const inputNameList = {
  group1: ['data1', 'data2', 'data3'],
  group2: ['test1'],
};
// 一覧の型の定義
export type State = {
  data1: string;
  data2: string;
  data3: number;
  test1: string;
};
// オブジェクトのkey部分を配列にて、取得
// 結果: ['group1', 'group2']
const objectList = Object.keys(inputNameList) as (keyof typeof inputNameList)[];
// 各種型の定義
// 結果: ('group1' | 'group2')[]
type InputGroupKeys = typeof validateGroupKeys;
// 結果: 'group1' | 'group2'
type InputGroupKeysNumber = typeof validateGroupKeys[number]
// enum化
const enum = convertObjectArrayToEnum<
  InputGroupKeys,
  InputGroupKeysNumber,
  State
>(inputNameList, objectList);
// 実際に使用
const {group1} = enum
// group1の中にdata1はあるので、OK
// <input {...register('data1')} />と結果は同じ
<input {...register(group1.data1)} />
// group1の中にdata100はないので、NG
<input {...register(group1.data100)} />

結論

今回作成した関数についてはこちらになります。

/**
 * Function to create an enum based on an array of names
 * @template { string[] } T Type of list of names in object first level
 * @template { string } S  List name type of object first level names
 * @param { T } keys List of names in object first level
 */
export function convertArrayToEnum<T extends unknown[], S extends string>(keys: T) {
  const result = {} as { [key in S]: S };
  keys.forEach((k: S) => {
    result[k] = k;
  });
  return result;
}

/**
 * Function to create an enum based on the name of an array in a property
 * @template { string[] } T Type of list of names in object first level
 * @template { string } S List name type of object first level names
 * @template { Object<string> } K  List name type of object first level names
 * @param { Object<S, string[]> } group group list
 * @param { T } keys List of names in object first level
 */
export function convertObjectArrayToEnum<T extends unknown[], S extends string, K extends object>(
  group: { [key in S]: string[] },
  keys: T
) {
  type InputLabel = { [key in keyof K]: string };
  const result = {} as { [key in S]: InputLabel };
  keys.forEach((k: S) => {
    result[k] = {} as InputLabel;
    group[k].forEach((value: string) => {
      result[k][value] = value;
    });
  });
  return result;
}

Nuxt2 から Nuxt3にアップデートを行う

個人で開発しているものをNuxt2からNuxt3にアップデートしたため、その時に対応したこと等を記載していきます。

Nuxt2からNuxt3にアップデートしようと思った理由について

Vue2が2023年末にサポートが切れてしまうためVue3にアップデートさせる必要があった

vue2が2023年12月31日にサポート終了 (EOL) でNuxt2がVue2のみのサポートのため、Vue3サポートがされているNuxt3にアップデートする必要があったため。

Vue2のサポートについて

v2.vuejs.org

Nuxt2, Nuxt Bridge, Nuxt3で対応されているものについて

nuxt.com

Nuxtの3.0がアップデートされた

2022/11/17にNuxt3.0にアップデートされたため、個人開発レベルのものであればNuxt3を使うことができると思ったため。

一部パッケージでVue3にしかサポートされていない

パッケージの最新版を使用する場合、Vue3にしか対応されていない & 新たにインストールするのに古いバージョンを使う理由もなかったため。

一例としては以下パッケージになります。

ja.splidejs.com

github.com

Nuxt3の機能について

nuxt.com

変更前と変更後の各種バージョン

before

{
  "@nuxt/components": "^2.2.1",
  "@nuxtjs/composition-api": "^0.32.0",
  "nuxt": "^2.15.8",
  "vue": "^2.6.14",
}

after

{
  "nuxt": "3.0.0",
}

Nuxt3へのアップグレード手順について

nuxt.com

変更した箇所について

  • npm scriptsの変更
  • configファイルの編集
  • VueファイルをNuxt3にあった書き方に変更
  • process.browserからprocess.clientに変更
  • layoutファイル等の書き方を変更
  • 動的なページのpathの書き方の変更

1. npm scriptsの変更

nuxtjs.org

変更した箇所は大きくNuxt.jsを新規作成する際にnuxt generateした結果を確認するコマンドnuxt previewが追加されました。

nuxt startnuxt previewの違いについて

stackoverflow.com

変更箇所

before

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt",
    "generate": "nuxt generate",
    "start": "nuxt start",
  },
}

after

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "start": "nuxt start",
    "preview": "nuxt preview",
  },
}

2. configファイルの編集

nuxtjs.org

nuxt.com

nuxt.com

今回対応した箇所については以下になります。

nuxtjs.org

srcDir: ソースファイルのroot部分の定義。Nuxt3の変更にあたっては変更はなし

nuxtjs.org

css: ページ全体で読み込まれるcssファイルの定義。Nuxt3の変更にあたっては変更はなし

nuxtjs.org

buildModules: 開発時やビルド時にのみ必要となるもののパッケージの一覧。Nuxt3の変更にあたって外部モジュールで使用しているものはなかったため、この項目自体削除。

nuxtjs.org

components: Nuxt.jsでコンポーネントファイルを自動で読み込みを行うかの設定。静的なコンポーネントのみの場合componentsの設定は不要だが、今回は動的に取得したいパターンもあるため、componentsを設定。 今回のコンポーネントの変更で component/Atoms/HogeFugaコンポーネントの場合、以下のように修正は必要。

before: <hoge-fuga />
after: <AtomsHogeFuga />

動的コンポーネントがある場合のcomponentの設定方法参考

zenn.dev

変更箇所

before

import type { NuxtConfig } from '@nuxt/types'

const config: NuxtConfig = {
  srcDir: 'src',
  css: ['~/assets/scss/main.scss'],
  buildModules: [
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    // https://go.nuxtjs.dev/stylelint
    '@nuxtjs/stylelint-module',
    // https://github.com/nuxt-community/composition-api
    '@nuxtjs/composition-api/module',
    // https://Vueuse.org/guide/index.html
    '@Vueuse/nuxt',
    // https://github.com/nuxt/components
    '@nuxt/components',
  ],
  components: (() => {
    const common = {
      extensions: ['Vue', 'ts'],
      ignore: ['**/_*', '**/constants.ts', '**/hooks.ts'],
    }
    return [
      { path: '~/components/Atoms/', ...common },
      { path: '~/components/Molecules/', ...common },
      { path: '~/components/Organisms/', ...common },
    ]
  })(),
}

export default config

after

import { resolve } from 'path'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  srcDir: 'src/',
  css: ['~/assets/scss/main.scss'],
  alias: {
    '~': resolve(__dirname, './src')
  },
  components: {
    global: true,
    dirs: ['~/components']
  }
})

3. VueファイルをNuxt3にあった書き方に変更

Nuxt3からVue3に対応されたため、composition-apiの書き方をする際に@nuxtjs/composition-apiのパッケージが不要に。 Vue3でsetup関数を使用することができるため、より簡単に記載することができるようになりました。 以下の2点も変更しています。

  • コンポーネントの名前を「ケバブケース(kebab-case)」から「パスカルケース(PascalCase)」に変更。
  • template部分にrootのノードが複数もつことができるようになった。

Vue3でのsetupの書き方について

v3.ja.vuejs.org

Vue3でのマルチルートノードコンポーネントについて

v3.ja.vuejs.org

変更箇所

before

<template>
  <div>
    <component-name1 />
    <component-name2 :array-list="arraySlice" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { array } from '~/filePath'

export default defineComponent({
  name: 'Page',
  setup() {
    const arraySlice = array.slice(0, 7)
    return { arraySlice }
  },
})
</script>

after

<template>
  <AtomsComponentName1 />
  <MoleculesComponentName1 :array-list="arraySlice" />
</template>

<script lang="ts" setup>
import { array } from '~/filePath'

const arraySlice = array.slice(0, 7)
</script>

4. process.browserからprocess.clientに変更

www.useful-blog.info

一部hooks部分でprocess.browserを使っていた箇所がNuxt3の変更に伴って動かなくなったため、process.clientに変更。

変更箇所

before

import { reactive, toRefs } from '@nuxtjs/composition-api'
import { useWindowScroll } from '@vueuse/core'
const { x, y } = 
  ? useWindowScroll()
  : { ...toRefs(reactive({ x: 0, y: 0 })) }

after

// Nuxt3のauto importにより、reactive, toRefsのimportは不要。
import { useWindowScroll } from '@vueuse/core'
const { x, y } = process.client
  ? useWindowScroll()
  : { ...toRefs(reactive({ x: 0, y: 0 })) }

5. layoutファイル等の書き方を変更

nuxt.com

Nuxt3でのlayoutファイル群の書き方も異なっていたため、以下のように変更しました。

変更箇所

before

layoutファイル側
<template>
  <nuxt />
</template>
pagesファイル側
<template>
  <component-name />
</template>

<script lang="ts">
import { defineComponent, useRoute, useMeta } from '@nuxtjs/composition-api'

export default defineComponent({
  name: 'PageName',
  layout: 'LayoutName',
  setup() {
    const route = useRoute()
    const paramId = Number(route.value.params.id)
    useMeta({
      title: 'ページタイトル名',
      meta: [{ hid: 'robots', name: 'robots', content: 'noindex' }],
    })
    return { paramId }
  },
  head: {},
})
</script>

after

layoutファイル側
<template>
  <slot />
</template>
pagesファイル側
<template>
  <NuxtLayout name="iframe">
    <ComponentName />
  </NuxtLayout>
</template>

<script lang="ts" setup>
const route = useRoute()
const paramId = Number(route.params.id)

useHead({
  title: 'ページタイトル名',
  meta: [{ hid: 'robots', name: 'robots', content: 'noindex' }]
})

definePageMeta({
  layout: false
})
</script>

6. 動的なページのpathの書き方の変更

nuxtjs.org

masteringnuxt.com

Nuxt3での動的ページの生成の方法も異なっていたため、以下のように変更しました。

変更箇所

before

File: /pages/users/_id.Vue
Url:  /users/123

$route.params = { id: "123" }

after

File: /pages/users/[id].Vue
Url:  /users/123

$route.params = { id: '123' }

今回は対応していないこと

1. storybookの対応

Nuxt3でのstorybookの対応がまだされていないため、今回のアップデートでは対応していません。

github.com

以下の方法で一部コンポーネントの表示は行うことはできましたがNuxtのauto importで読み込みを行っているものがエラーになってしまったため、今回は見送りました。

zenn.dev

<template>
  <div class="hoge" ref="ref">
    コンテンツ
  </div>
</template>

<script lang="ts" setup>
  // ここでrefの読み込みがないとのエラーが出る。
  const ref = ref()
</script>

cssの「:where」と「:is」の違いを理解する

css:where:isの違いについて理解できていなかったため、違いなどについてまとめていこうと思います。

MDNの情報から

:whereについて

developer.mozilla.org

:where() は CSS の擬似クラス関数で、セレクターリストを引数として取り、列挙されたセレクターのうちの何れかに当てはまるすべての要素を選択します。

/* ヘッダー、メイン、フッターの何れかの中にある段落に
   カーソルをかざしたときに選択 */
:where(header, main, footer) p:hover {
  color: red;
  cursor: pointer;
}

/* 上記のものは下記のものと同等です。 */
header p:hover,
main p:hover,
footer p:hover {
  color: red;
  cursor: pointer;
}

:isについて

developer.mozilla.org

:is() は CSS の擬似クラス関数で、セレクターのリストを引数に取り、リスト中のセレクターの何れか一つに当てはまる要素をすべて選択します。数多くのセレクターを小さくまとめて書くのに便利です。

/* header, main, footer 要素の中の段落で
   マウスポインターが通過中のものをすべて選択 */
:is(header, main, footer) p:hover {
  color: red;
  cursor: pointer;
}

/* 上記のものは下記のものと同等です。 */
header p:hover,
main p:hover,
footer p:hover {
  color: red;
  cursor: pointer;
}

:where:isの違いについて

developer.mozilla.org

この 2 つの違いは、 :is() がセレクター全体の詳細度にカウントされる(最も詳細な引数の詳細度を取る)のに対し、 :where() は詳細度の値が 0 であることです。これは、 :where() 参照ページの例で実証されています。

今回検証したものについて

classの上書きについて

  • 「テキスト」の部分が対象
  • 上2つに.cardのclassを付与。.cardに対して黒色を指定
  • :isに赤色を指定
  • :whereに青色を指定

See the Pen where and is by miwa_shuntaro (@miwashutaro0611) on CodePen.

「:is」のちょっとした例について

  • cardという同じUIスタイル
  • aタグの場合は文字色を赤色・その他の場合はスタイルを適応させない
    • 今回は文字色でやっていますが、ホバー時の追加などしたい時に有効かもしれないです。

See the Pen link text add color by miwa_shuntaro (@miwashutaro0611) on CodePen.

「:where」のちょっとした例について

  • 「タイトル」の部分が対象
  • class名部分で font-size: 20px、h1部分でfont-size: 10pxで宣言している

See the Pen title reset where by miwa_shuntaro (@miwashutaro0611) on CodePen.

sanitize.css:whereを用いたスタイルのリセットを用いられています。

github.com

結論

  • リセットcssとして使用するのであれば、:where
  • 指定の条件(aタグでhogeのclass名がついているとき)には:is

Deno のフルスタック・ウェブフレームワーク「Fresh」を触ってみる

Freshについて少し調べてみたので、その時のメモになります。

Freshとは

deno.com

Fresh is a new full stack web framework for Deno. By default, web pages built with Fresh send zero JavaScript to the client. The framework has no build step which allows for an order of magnitude improvement in deployment times. Today we are releasing the first stable version of Fresh.

実行手順

fresh.deno.dev

以下のコマンド通り実行し、 http://localhost:8000/を開くと以下のような画面になります。

$ deno run -A -r https://fresh.deno.dev my-project
$ cd my-project
$ deno task start

補足

denoのエラーがでる場合

denoをインストールしていない場合は以下のようなエラーが出るため、denoのインストールが必要です。

エラー内容

$ deno run -A -r https://fresh.deno.dev my-project
zsh: command not found: deno

denoのインストールについて

yoshixmk.github.io

macの場合は以下でインストールできます。

$ brew install deno

deno run -A -r https://fresh.deno.dev my-projectを実行した際の挙動

deno run -A -r https://fresh.deno.dev my-projectを実行した場合、以下のようにダウンロードされます

$ deno run -A -r https://fresh.deno.dev my-project
Download https://fresh.deno.dev/
Download https://deno.land/x/fresh@1.0.2/init.ts
Download https://deno.land/x/fresh@1.0.2/src/dev/deps.ts
Download https://deno.land/x/fresh@1.0.2/src/dev/error.ts
Download https://deno.land/x/fresh@1.0.2/src/dev/mod.ts
Download https://deno.land/std@0.150.0/flags/mod.ts
Download https://deno.land/std@0.150.0/fs/walk.ts
Download https://deno.land/std@0.150.0/path/mod.ts
Download https://deno.land/std@0.150.0/semver/mod.ts
Download https://deno.land/std@0.150.0/_util/assert.ts
Download https://deno.land/std@0.150.0/fs/_util.ts
Download https://deno.land/std@0.150.0/_util/os.ts
Download https://deno.land/std@0.150.0/path/_interface.ts
Download https://deno.land/std@0.150.0/path/common.ts
Download https://deno.land/std@0.150.0/path/glob.ts
Download https://deno.land/std@0.150.0/path/posix.ts
Download https://deno.land/std@0.150.0/path/separator.ts
Download https://deno.land/std@0.150.0/path/win32.ts
Download https://deno.land/std@0.150.0/path/_constants.ts
Download https://deno.land/std@0.150.0/path/_util.ts
Do you want to use 'twind' (https://twind.dev/) for styling? [y/N] y
Do you use VS Code? [y/N] y
The manifest has been generated for 3 routes and 1 islands.

freshのファイル内容について

ディレクトリ全体については以下のようなイメージになります。

my-app/
├── README.md
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│   └── Counter.tsx
├── main.ts
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
└── static
    ├── favicon.ico
    └── logo.svg

dev.ts

プロジェクトの開発エントリポイント

main.ts

プロジェクトのプロダクションエントリーポイント

fresh.gen.ts

routesislandsに関する情報を含むマニフェストファイル

import_map.json

プロジェクトの依存関係を管理するために使用するインポートマップ

deno.land

deno.json

プロジェクトの依存関係ファイル(import_map.json)の読み込みやプロジェクトで実行したいコマンドの登録を行うファイル

routes/

ページを開く際のルートフォルダ。設定箇所で開くページについては以下を参照

  • routes/index.tsx: http://localhost:8000/
  • routes/[name.]tsx: http://localhost:8000/hoge
    • hgoeの部分は任意で設定可能

fresh.deno.dev

fresh.deno.dev

islands/

クライアントサイドのインタラクティブ性を実現するためのファイル。 JavaScriptの読み込みが大きい場所などで使用していない箇所はScriptタグの読み込みを行わないことにより、ページのパフォーマンスを効率化することができます。

fresh.deno.dev

fresh.deno.dev

static/

静的なファイルを格納するフォルダ。

fresh.deno.dev

デプロイについて

GitHubにpush, deno deployで設定を行うことでデプロイすることができます

fresh.deno.dev

最後に

初期からルーティングのリンクの追加したのみですが、デプロイしたものについてはこちらになります。

https://miwa-fresh-sample.deno.dev/

eslintを使用してimportの部分をいい感じにする

以下のルール・パッケージを使用するといい感じにimport文の整形を行うことができたので、設定方法などを記載したものになります。

※ 執筆時点(2022/07/18)の情報になるため、ご認識ください。

結論としては以下のルール・パッケージになります。

ルール・パッケージについて

typescript-eslint.io

www.npmjs.com

実行結果

eslint --fixを実行した場合

変更前

import './App.css';
import { TypeHoge } from '~/types/Hoge';
import { useState, FC } from 'react';
import { ComponentHoge } from '~/components/Hoge';

変更後

import './App.css';
import type { FC } from 'react';
import { useState } from 'react';
import { ComponentHoge } from '~/components/Hoge';
import type { TypeHoge } from '~/types/Hoge';

1. consistent-type-imports

typescript-eslint.io

{
  rules: {
    // ここから記載
    "@typescript-eslint/consistent-type-imports": [
      "error",
      {
        prefer: "type-imports"
      }
    ],
    // ここまで記載
  },
}

設定できるプロパティについて

type Options = {
  prefer: 'type-imports' | 'no-type-imports';
  disallowTypeAnnotations: boolean;
};

const defaultOptions: Options = {
  prefer: 'type-imports',
  disallowTypeAnnotations: true,
};

prefer

typescript-eslint.io

importしたtypeに対して、typeを付与するかしないか

type-importsの場合

import type { Foo } from 'Foo';
import type Bar from 'Bar';
type T = Foo;
const x: Bar = 1;

no-type-importsの場合

import { Foo } from 'Foo';
import Bar from 'Bar';
type T = Foo;
const x: Bar = 1;

disallowTypeAnnotations

typescript-eslint.io

trueの場合、以下のコードだとエラーになります。

type T = import('Foo').Foo;
const x: import('Bar') = 1;

2. eslint-plugin-import

eslintのimport文の整形をしてくれるパッケージです。

$yarn add --save-dev eslint-plugin-import

各種ルールについて

全体は省略。

今回設定しているものについては以下になります。(デフォルトのルールではありますが、後々パッケージ側で変更されても問題ないように個別にも記載しています。)

{
  rules: {
    "import/order": [2, { "alphabetize": { "order": "asc" } }]
  },
}

import/order

github.com

今回の場合は以下のようなイメージでの並び替えになります。

  • アルファベット順
  • 昇順

変更前

import React, { PureComponent } from 'react';
import aTypes from 'prop-types';
import { compose, apply } from 'xcompose';
import * as classnames from 'classnames';
import blist from 'BList';

変更後

import blist from 'BList';
import * as classnames from 'classnames';
import aTypes from 'prop-types';
import React, { PureComponent } from 'react';
import { compose, apply } from 'xcompose';

TypeScriptと併用する場合、以下のように設定

www.npmjs.com

$  yarn add --dave-dev @typescript-eslint/parser
{
  extends: [
    // なにかのルール
    "plugin:import/typescript" // 追記
  ],
  plugins: [
    'import', // 追記
  ],
}

React.js + TypeScript環境でnpx eslint --initを実行し、以下の選択肢を選んだ場合 + 上記の設定を行ったものになります。

※ Vue.jsや通常のTypeScriptの場合でも上記と同様の方法で入れることができます。

ESLintの設定

✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · react
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · yarn

ESLintのコード

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    'plugin:react/recommended',
    'airbnb',
    "plugin:import/typescript"
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: [
    'react',
    '@typescript-eslint',
    'import',
  ],
  rules: {
    "@typescript-eslint/consistent-type-imports": [
      "error",
      {
        prefer: "type-imports"
      }
    ],
    "import/order": [2, { "alphabetize": { "order": "asc" } }]
  },
};

Next.jsでLighthouse CIを導入して、PRの時点でLighthouseのスコアを確認できるようにする

Next.jsでのプロジェクトでLighthouse CIを確認できるようにしたので、その時に行ったことになります。

参考にした記事

blog.kimizuy.dev

github.com

Lighthouseとは

Webアプリのパフォーマンス、品質、および正確性を向上させるためのオープンソースの自動化ツールです。

chrome.google.com

Lighthouse is an open-source, automated tool for improving the performance, quality, and correctness of your web apps.

こちらのようなイメージで各種の状態を確認することができます。

今回のゴール

PR作成時点で以下のようになる

  • ci/circle: lhciの部分にビルドの結果が表示

  • Detailsの部分をクリックすると以下のようにlighthouseの結果が表示される

実装した方法

1. Github Actionsに「Lighthouse CI 」をインストール

以下のページからインストールを行う

github.com

2. CIの記載

以下のページから実行したいものを選択し、コードの記載

github.com

対応しているものは以下になります。

circleCIの場合は以下になります。

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/node:15.12-browsers
    working_directory: ~/your-project
    steps:
      - checkout
      - run: npm install
      - run: npm run build
      - run: sudo npm install -g @lhci/cli@0.8.x
      - run: lhci autorun

3. .lighthousercなどのlighthouse用の設定ファイルを記載

以下の部分を参考に、.lighthousercの設定を行う

github.com

4. build, startコマンドがない場合、追加

ci上で計測を行うため、build, startのコマンドを追加

"scripts": {
  "build": "next build",
  "start": "next start"
},

startについては以下のタイミングで呼び出されています。

以上でPR作成時にlighthouseが追加されるようになります。

詰まった箇所について

1. Chrome installation not foundのエラーが出る

こちらについては以下のissuesを参考に--collect.settings.chromeFlags=--no-sandboxを追加

github.com

// その他の処理については省略
module.exports = {
  ci: {
    collect: {
      settings: { chromeFlags: '--no-sandbox' }, // これを追加
    },
  },
}

2. circleci/node:16などの-browsersでないとビルドエラーになる

dockerのimageがcircleci/node:16の場合、以下のエラーがでるため、修正

// NG
docker:
  - image: circleci/node:16
// OK
docker:
  - image: circleci/node:16-browsers