dtaniguchi.com

blog

yarn から npm に戻した話 ① – Storybook が起動しない編

d.taniguchi

2023年10月6日 6:19

Post / コメント

  • Node.js
  • npm
  • Storybook

引き続き、Vue3 による新規開発プロジェクトで仕事をしている。

プロジェクト基盤の整備が終わり、コンポーネント開発に着手した。 Composition API でボタンや入力コンポーネントを作成したり、Router に Page コンポーネントと Path の紐付けを追加したり、といった作業を進める。

Vue Router4 に Nested Routes があるのは嬉しかった。今回 Modal 表示を Path で制御したい箇所がある。Next.js の App Router では Layout 作成がしやすかった。この機能は積極的に使いたい。

いくつかコンポーネントができたので、 Storybook7 を導入して作成したコンポーネントを登録することにした。まずは Local 上で Project に Storybook を Install 、正常に起動した。続いて docker-compose.yml にコンテナを追加して起動しようとしたとき問題が発生した。起動しないのである。またかい。Vite が Docker で動作しないに引き続きである。

Dockerfile は以下のような内容だ。

Dockerfile
FROM node:18.18 AS development

ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN apt-get update

WORKDIR /app
COPY ["package.json", "yarn.lock", "./"]

RUN yarn

エラー表示は以下の通り。

storybook  | 🔴 Error: It looks like you are having a known issue with package hoisting.
storybook  | Please check the following issue for details and solutions: https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092
storybook  | 
storybook  | 
storybook  | /app/node_modules/cli-table3/src/utils.js:1
storybook  | const stringWidth = require('string-width');
storybook  |                     ^
storybook  | 
storybook  | Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/string-width/index.js from /app/node_modules/cli-table3/src/utils.js not supported.
storybook  | Instead change the require of index.js in /app/node_modules/cli-table3/src/utils.js to a dynamic import() which is available in all CommonJS modules.
storybook  |     at Object.<anonymous> (/app/node_modules/cli-table3/src/utils.js:1:21)
storybook  |     at Object.<anonymous> (/app/node_modules/cli-table3/src/table.js:2:15)
storybook  |     at Object.<anonymous> (/app/node_modules/cli-table3/index.js:1:18)
storybook  |     at Object.<anonymous> (/app/node_modules/@storybook/core-server/dist/index.js:66:2799)
storybook  |     at Object.<anonymous> (/app/node_modules/@storybook/cli/dist/generate.js:11:4494)
storybook  |     at Object.<anonymous> (/app/node_modules/@storybook/cli/bin/index.js:26:1)
storybook  |     at Object.<anonymous> (/app/node_modules/storybook/index.js:3:1) {
storybook  |   code: 'ERR_REQUIRE_ESM'
storybook  | }

御丁寧なことにエラーに直接 Issue の URL が明記されている。世の中にそんなエラー表示があったのか。

https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092

Issue を要約すると次のようになった。

  • yarn ではなく、 pnpm を使用したら動いた
  • yarn ver. 1 系のバグ
  • yarn ver.3 系にアップグレードすると直る
  • yarn.lock がある状態で yarn コマンドを実行すると依存性解決に失敗する
  • package.json だけの状態から yarn コマンドを実行すると依存性解決は成功する

試しに正常動作している Local の node_modules を削除して yarn コマンドを再実行すると、 問題が出現した。原因は Docker ではなく yarn だった。yarn install コマンドでは yarn.lock ファイルがある場合と、ない場合で異なる処理系を用いているのだろうか。悪い設計の典型だ。

依存性解決に yarn.lock ファイルを利用できないということは、コンテナイメージが作られる度に、 Install されるライブラリの Version が微妙に異なるコンテナができあがることを意味する。仮にE2E テストが失敗しても、テストに使ったコンテナが消えた途端、テストが失敗したライブラリの Version 情報が失われることになる。こんな方式は採用できない。

呆れながら npm i コマンドを実行し、node_modules を削除後 npm ci コマンドを実行したら、 Storybook は正常に立ち上がり、 Docker 上でも問題なく動いた。

Vue3 や Storybook の公式を見てみると npm をデフォルトとして表示している。 Storybook 公式での並び順は、今回の Issue を反映しているかのようだ。もう yarn を使用する理由はないようだ。

私のプロジェクトでも、すべて npm で作業を進めることにした。Dockerfile やドキュメント類のコマンドはすべて yarn から npm に戻した。

型安全芸術への招待 – TypeScript で Object の const assertion と Key, Value の型指定が気に入っている件

d.taniguchi

2023年10月1日 21:31

Post / コメント

  • Java
  • Tips
  • TypeSafetyArt
  • TypeScript

学生の頃、趣味で VBA や COBOL を書いていたのだが、仕事で書くようになったのは Java であった。Java で学んだことは今も血肉として活きているし、代えがたい愛着もある。Java の好きな点はいくつもあるが、とりわけ好きだったのが Enum である。

よくこんなコードを書いた。

Options.java
public interface Options<K extends Serializable, V extends Serializable> {
    K getKey();
    V getValue();

    static <K extends Serializable, ENUM extends Enum<ENUM> & Options<K,?>> ENUM getEnumFromKey(Class<ENUM> clazz, K key) {
        for (ENUM enm : clazz.getEnumConstants())
            if (key.equals(enm.getKey())) return enm;
        throw new IllegalArgumentException();
    }

    static <V extends Serializable, ENUM extends Enum<ENUM> & Options<?,V>> ENUM getEnumFromValue(Class<ENUM> clazz, V value) {
        for (ENUM enm : clazz.getEnumConstants())
            if (value.equals(enm.getValue())) return enm;
        throw new IllegalArgumentException();
    }
}
Enums.java
public class Enums {
    public enum Sex implements Options<Integer, String> {

        FEMALE(0, "女性"),
        MALE(1, "男性");

        private final int key;
        private final String value;
        Sex(int key, String value) { this.key = key; this.value = value; }
        @Override public Integer getKey() { return key; }
        @Override public String getValue() { return value; }
    }
}
User.java
class User {
    @Getter
    @Setter
    private String name;

    @Getter
    private Enums.Sex sex;

    @Getter
    @Setter
    private int age;

    public void setSex(int sex) {
        this.sex = Options.getEnumFromKey(Enums.Sex.class, sex);
    }
}

上記コードは DB から取得した User Entity が、取得時点で int 定数を Enum に変換、カプセル化される様子を再現している。

Java の Enum は Singleton なので、データの一意性や型安全性、不変性を保証するときに強力な効果を発揮する。データ入力時点には意味も型安全も持たなかった int 定数は、意味も型安全も不変性も一意性も有している Enum で Field に保持されるので、それ以降の処理で定数やラベルの誤りなど発生しようがない。

Generics を使用することで、動的型付け言語と同じ型指定の柔軟性を持たせつつ、型の誤りは実装の段階で検出できる。これは動的型付け言語が逆立ちしても実現できない、型安全のためのチェック網だ。これはもう静的型付け言語による、型安全芸術と呼んでよろしかろう。

Java が魅せるオブジェクト指向と静的型付けの世界に耽溺していた私には、 当初 TypeScript に対してよい印象を持つことは難しかった。JavaScript との相互変換を意識するあまり、静的型付け言語としてはあまりに型安全性が脆弱な、”妥協的産物”というイメージがあったからである。例を挙げるなら Union 型だろう。

TypeScript
function getYearOfBirth(age: string|number) {
    let numericAge
    if (typeof age === "string") {
        numericAge = Number(age)
        if (Number.isNaN(numericAge)) {
            throw Error()
        }
    } else {
        numericAge = age
    }
    // 以降省略

この見苦しい関数は何だ。なぜ継承関係がない string 型と number 型の両方が、同じ関数の同じ引数に指定できてしまうのだ?言語仕様がリスコフの置換原則を否定しているではないか。これでは静的型付け以前に、オブジェクト指向言語と言えるのかすら怪しくはあるまいか。

今考えると、言語仕様を妥協したとしても、JavaScript と完全にトランスパイルできるようにしたのは、正しい判断だったと思っている。現実世界では、理想を主張する方が簡単だ。TypeScript を厳格な静的型付け言語に設計し、TypeScript から JavaScript へ変換できても、JavaScript のライブラリは TypeScript で使用不可能だったら、これほど広く使用される言語になったかどうか疑わしい。使われないものに価値はない。 Alt JS のデファクトスタンダード登場が遅れていたかもしれない。Microsoft 社の TypeScript 開発チームのこの英断こそ、プロの仕事と呼ぶに相応しい。

特に面白みもなく書いていた TypeScript が、私の中で輝きを放つきっかけとなったのが Literal 型であった。

TypeScript
type Fruit = "apple" | "orange" | "grape" | "banana"

Literal 型を初めて知ったときは驚愕した。まさか string や boolean といった型にまだ subtype を定義できる余地が残っていたとは思いもよらなかったからだ。Anders Hejlsberg の仕業だろうか。才能豊かな言語設計者の発想には頭が下がる。

Literal 型は Enum と似ている。似ているが 仰々しい Java の Enum より遙かにシンプルな Syntax だ。これはとんでもなくよいアイデアではあるまいか。

TypeScript
type user = {
  name: string,
  age: number
  favoriteFruit: Fruit
  ...
}

これを Key Value 形式のデータ構造である Object 型に応用すれば、上述した Java の Enum と同じような役割を与えることが可能になるではないか。

TypeScript では Object の key を ` <T extends Object> keyof T ` 、value を ` <T extends Object> T[keyof T] `として型定義することができる。

TypeScript
export type Options<K extends number|string, V> = Readonly<Record<K, V>>

export type OptionsKey<T extends Options<any, any>> = keyof T

export type OptionsValue<T extends Options<any, any>> = T[keyof T]


/**
 * Options の value から key を取得する
 *
 * @param options
 * @param value
 * @returns key
 */
export const getOptionsKeyFromValue = <T extends Options<any, any>, K extends keyof T, V extends T[keyof T]>(options: T, value: V): K =>
   (Object.entries(options).find(el => el[1] === value) as [K, V])[0]
TypeScript
export const SEX = {
  0: "女性",
  1: "男性"
} as const satisfies Options<number, string>
TypeScript
export type User = {
  name: string
  sex: OptionsKey<typeof SEX>
  age: number
}

これにより格納される値はただの int 定数であったとしても、コード上は意味が明確に定義された型安全のある別物となった。例えば、テストや Storybook の引数設定などで、Field 値が必要になったとしても、型だけ分かっていれば実値は必要ない。

TypeScript
const user: User = {
  name: "test taro",
  sex: getOptionsKeyFromValue(SEX, "男性"),
  age: 23
}

この関数、さらに Literal 型の効力で選択肢が Suggest される。

何という便利、何という型安全性だろう。これこそ私が目指す型安全芸術だ。ただの JavaScript の Object が、TypeScript の型機能によって、別次元のものとなった。

Vite が Docker で動作しない

d.taniguchi

2023年9月23日 22:55

Post / コメント

  • Docker
  • Tips
  • Vite
  • Vue3

Vue3 による新規開発プロジェクトが始まり、プロジェクト基盤の整備を行っている。

Scaffolding tool で 初期 Project を生成、Router や Linter、Validator 等、使用ライブラリの選定・設定や、環境毎の DI 設定など、前準備の真最中だ。

概ね必要な部品群は揃い、いよいよ Dockerfile を追加して docker-compose で画面が立ち上がれば準備完了という段に来て、これまで堅調に進んでいた構築作業は水を差された。これは恒例行事だった。Docker の設定、この作業がスムーズにいった試しはない。悪いことに、今回はいつも以上にしぶとい抵抗を見せるのだった。どうやっても Vite が下記のエラーで立ち上がらない。

Segmentation fault

この時の Vite の Version は 4.4.9 、Docker for Mac の Version は 4.23 であった。調べてみると Docker の GitHub リポジトリに Issue が立っていた。

https://github.com/docker/for-mac/issues/6824

このエラー、 Docker の開発陣にも原因がよく分かっていないようだった。以下条件で発生するという。

  • Intel Mac の一部機種
  • Node Version 18 〜 20 で実行
  • Docker for Mac Version 4.19 以上
  • Docker ではなく、 Mac の Hypervisor の問題

CPU に Core i7 や i9 を搭載している機種で多く発生していて、 i5 だと動くことが多いようである。私の使用していた iMac は i9 だ。手元に i5 搭載の MacBook があったのでこちらにリソースを移して docker-compose をたたいてみたらすんなり動いた。

問題が発生しても対応は簡単で、Docker の GUI から Setting > General 内にある VirtioFS のチェックを osxfs(Legacy) に変更、Use Virtualization framework のチェックを外せばいいだけである。

これにより Vite が立ち上がるようになった。

React18 にするか、Vue3 にするか

d.taniguchi

2023年9月22日 19:14

Post / コメント

  • React
  • Vue3

ありがたいことに、2023年9月より再び Frontend のリードエンジニアとして新規開発プロジェクトに参画している。しかも、今回 Frontend の担当者は私1人だけだ。

CTO、BE、FE にそれぞれ一人、総勢3名の小さなチーム。開発予定のソフトウエアはまだ構想のみ、要件定義も、画面デザインも何もないところからスタートである。私の最初の課題は技術選定だ。

仕様を考慮すると SPA が最善という結論に至ったが、フレームワークを React 18 にするか Vue 3 にするかはすぐ選ぶことができなかった。 Web 業界では、 Vue が一世を風靡したかと思ったら、また React へ軸足を戻してしまった。理由はよくわかる。 Vue がとどまることを知らない素晴らしい進化を続けるからだ。 Vite を生み出し、あっという間に TypeScript に対応、さらにデフォルトの API まで変更したと思ったら、 Vue2 の 2023 年末 EOL まで決めてしまった。背筋の凍る早さだ。 Vue3 には破壊的変更がある。Vue2 からたやすく移行を行えない。

今回案件探しをしている中に、 Vue2 から Vue3 への移行プロジェクトがあった。 Vue3 に触ってみたいだけの気軽さで応募したところ、「移行経験ない人お断り!」とけんもほろろに断られてしまった。この切迫感、当然である。いつ自分たちで修正不能なクリティカルレベルの脆弱性が見つかるとも知れないプロダクトが本番環境で動いていると思うとゾッとする。いや、むしろもうすでに山のような・・・。

Vue2 の公式では、やや申し訳なさそうに Vue3 はどうしても破壊的変更が不可避だったこと、 Vue2 の EOL 、そして今後のリリースは、下位互換を大切にしたいとする思いが語られている。

https://v2.vuejs.org/lts/#Upgrade-to-Vue-3

信用できない。長期的な運用を考慮するとやはり Vue を選ぶのは怖い。 React を Vite で動かすのが現状最善と考え CTO に提案したら、是非 Vue を採用してほしいと頼まれてしまった。

かくして私は Vue3 開発を行うことになったのだった。React であれば新鮮さもないが、Vue3 だと新しいことだらけだ。TypeScriptへの対応ぶり、Composition API の書き心地など、今から楽しみなのである。

推し Code formatter

d.taniguchi

2023年9月15日 23:43

Post / コメント

  • ESLint
  • Tips

9月より新しい新規開発プロジェクトに参画するのだが、契約手続きの関係でスタートが1日からではなく19日からになった。開始までにまだ時間がある。私は少し先回りして、普段はあまり時間をかけられないプロジェクト基盤周辺部に、じっくり取り組みたい心持ちになった。

技術スタックの検討が済めば、その後は VSCode の .setting.json にはじまり、Dockerfile、GitHub Actions、README.md への細々とした記述など、初期プロジェクトの作成者には準備しなければならないファイルがたくさんある。 Linter や Code formatter の設定も、そういったもののうちの一つだ。

直近参加した開発プロジェクトのいくつかは、いずれも Code formatter に Prettier を使用していたが、よい印象がない。私はソースコードの改行箇所と改行数を強制されるのが好きではなかった。関数内と関数間の改行数は変えたいし、文字数で強制的に改行されるのも1行に意味を持たせたい時に邪魔になる。

カスタマイズしてみようと Package を install してみたが、ダメだと気づくのに時間はかからなかった。設定項目がたった24種類しかないのだ。勝手に Format されるが、その内容のほとんどは条件の有無を設定できない。いらん。こんなものを Project に入れる気にならなかったので削除していたら、 ESLint に Code formatter の機能があることを知った。しかも ESLint なら、すべての条件で有効・無効の設定できるという。

https://eslint.org/docs/latest/rules/

設定項目は非推奨になっていないものだけで200種類もある。これだ。これこそ私の探し求めていたものだ。折角である。200種類すべて試してみる。

.eslintrc.cjs
/**
 * ESLint 設定
 *
 * @see https://eslint.org/
 */

module.exports = {
  root: true,
  extends: [
    "eslint:recommended",
  ],
  parserOptions: {
    ecmaVersion: "latest"
  },

  /**
   * カスタムルール
   *
   * [eslint]
   * @see https://eslint.org/docs/latest/rules
   *
   * @see https://typescript-eslint.io/rules
   *
   */

  rules: {

    /*****************
     * 構文に関する設定
     *****************/


    /*
     * [warn]
     *   開発中は使ってもいいが、本番までに削除
     */

    // console の使用: 警告
    "no-console": "warn",
    // alert の使用: 警告
    "no-alert": "warn",
    // 同一依存先から複数行の import: 警告
    "no-duplicate-imports": "warn",
    // () を使用する演算の省略: 警告
    "no-constant-binary-expression": "warn",


    /*
     * [error]
     *   開発、本番いずれも使用しない
     */

    // eval の使用: エラー
    "no-eval": "error",
    // 型のチェックを厳密に行えない equal ( == or != ) の使用: エラー
    "eqeqeq": ["error", "smart"],
    // Camel case 以外の変数、関数命名: エラー
    "camelcase": "error",


    // Array<T> -> T[]: エラー
    "@typescript-eslint/array-type": "error",



    /*************************************
     * Code Format に関する設定 (auto fix)
     *************************************/

    /*
     * [warn] 自動修正されるので、すべて warn 指定
     */

    // セミコロン: 不要
    "semi": ["warn", "never"],
    // タブ: 使用不可
    "no-tabs": "warn",
    // タブ・スペースの混在: タブを使用禁止にしているので off
    "no-mixed-spaces-and-tabs": "off",
    // 行末の余計なスペース: 削除
    "no-trailing-spaces": "warn",
    // インデント: 2文字
    "indent": ["warn", 2, { "SwitchCase": 1 }],
    // 改行コード: LF
    "linebreak-style": ["warn", "unix"],


    // 1行の文字数: 制限なし
    // https://eslint.org/docs/latest/rules/max-len
    "max-len": "off",


    // 空白行: 7行まで
    "no-multiple-empty-lines": ["warn", { "max": 7, "maxEOF": 0 }],
    // 最終空白行: 必須
    "eol-last": "warn",

    // 文字列リテラル: Double Quotation
    "quotes": "warn",

    // コンマの後のスペース: 必須
    "comma-spacing": "warn",
    // 最後の要素に付与するコンマ: 複数行の場合のみ
    "comma-dangle": ["warn", "only-multiline"],
    // コンマの位置: 後ろにのみ付与
    "comma-style": ["warn", "last"],

    // [] にスペース: 削除
    "array-bracket-spacing": ["warn", "never"],
    // {} にスペース: 必須
    "block-spacing": ["warn", "always"],
    // Object の key 定義 コロン前後のスペース: 後ろのみ
    "key-spacing": "warn",
    // オブジェクトのプロパティ obj[name] 空白: 削除
    "computed-property-spacing": "warn",

    // ブロック改行
    "brace-style": "warn",
    // 不要な括弧: 削除
    "no-extra-parens": "warn",
    // 不要なスペース: 削除
    "no-multi-spaces": "warn",
    // ブロック前後の空白: 必須
    "keyword-spacing": "warn",

    // 関数定義の関数名と引数の間のスペース: 削除
    "space-before-function-paren": ["warn", {
      "anonymous": "always",
      "named": "never",
      "asyncArrow": "always"
    }],
    // 関数定義の改行: 一貫性を強制
    "function-call-argument-newline": ["warn", "consistent"],
    // arrow 演算子 => の前後にスペース: 必須
    "arrow-spacing": "warn",


    // 初期値が設定された変数、定数に対する型宣言: 削除
    "@typescript-eslint/no-inferrable-types": "warn",

  },
}

かくして私の担当する新しい Project に相応しい、私の好みがつまった Code formatter が誕生したのだった。