Madogiwa Blog

主に技術系の学習メモに使っていきます。

eslint-plugin-unicornの導入してみたので設定方法とかメモ📝

eslint-plugin-unicornを個人のプロジェクトに導入してみたので導入方法とかについてメモ📝

eslint-plugin-unicornとは?

eslint-plugin-unicornはモダンなJavaScript/TypeScriptの書き方を促進するためのESLintプラグインで、ESLintには無いモダンな記法やファイル名に関する静的解析なども提供しているプラグインです。

github.com

導入方法

1. インストール

# pnpmを使う場合
pnpm add -D eslint-plugin-unicorn

# npmを使う場合
npm install --save-dev eslint-plugin-unicorn

2. ESLint設定ファイルの構成

最近のESLintフラットコンフィグ形式(ESLint v9.0.0以降)を使った設定方法ですがeslint.config.jsに以下のように追加します。以下は推奨ルールをデフォルトで設定する例です。

import unicorn from "eslint-plugin-unicorn";

export default [
  // 他のプラグインやルール設定
  unicorn.configs.recommended
];

これでeslint-plugin-unicornのオススメルールが有効になった状態になります🦄

主要なルールと特徴

eslint-plugin-unicornには多数のルールがありますが、特に気になったものをいくつか紹介します。

全てのルールは以下を参照してください。

https://github.com/sindresorhus/eslint-plugin-unicorn?tab=readme-ov-file#rules

モダンなAPIの使用を促進するルール

  • prefer-array-flat: ネストした配列をフラット化する際にArray#flat()を使用するよう促します
  • prefer-string-slice: 非推奨のString#substr()String#substring()の代わりにString#slice()を使用するよう促します
  • prefer-string-replace-all: 正規表現のグローバルフラグを使った置換よりもString#replaceAll()を使用するよう促します

コードの品質向上に関するルール

  • no-useless-undefined: 不要なundefinedの使用を禁止します
  • prefer-ternary: シンプルなif-else文の代わりに三項演算子を推奨します
  • explicit-length-check: 配列やオブジェクトの長さを明示的に確認することを強制します

ファイル名規則に関するルール

  • filename-case: ファイル名に一貫した命名規則を適用します(camelCase、PascalCaseなど)

一部のルールをカスタマイズする

基本的にESLintのその他のルールと同様ですが、すべてのルールがすべてのプロジェクトに適していると言うわけではないので、プロジェクトの特性に合わせて一部のルールを調整することができます。私は以下のような調整を行いました。

{
  rules: {
    // ファイル名の規則をカスタマイズ
    "unicorn/filename-case": [
      "error",
      {
        cases: {
          // プロジェクトで許可する命名規則
          camelCase: true,  // myComponent.js
          pascalCase: true, // MyComponent.js
        },
      },
    ],
    // 略語を許可(prevent-abbreviationsは厳しすぎる場合が多い)
    "unicorn/prevent-abbreviations": "off",
  }
}

まとめ

eslint-plugin-unicornは、モダンなJavaScript/TypeScriptコードにいい感じに矯正してれるのと、ファイル名のルールとかカスタムルールとか作ってましたがデフォルトで提供されてるのもいい感じだなと思いました!

ESLintにはsuppress ruleの仕組みも導入されたのでとりあえず既存のプロジェクトにサクッと導入してみて一旦suppressしておき徐々に対応していくような進め方も良さそうですね🦄

eslint.org

`js-tiktoken`を使ってJavaScriptでOpenAIのトークン数を計算する方法メモ📝

OpenAIのAPIリクエストを行う際には、トークン数の制限やコスト計算のために正確なトークン数を把握することが重要です。今回はフロントエンド(JavaScript/TypeScript)でトークン数を計算するためにjs-tiktokenライブラリを導入したのでその方法をメモしておきます📝

tiktokenとは?

If you need a programmatic interface for tokenizing text, check out our tiktoken package for Python. For JavaScript, the community-supported @dbdq/tiktoken package works with most GPT models. https://platform.openai.com/tokenizer

tiktokenは、OpenAIが公開しているトークナイザーのPython実装です。OpenAIのモデル(GPT-3.5, GPT-4など)が使用するトークン分割ロジックを再現しています。

github.com

JavaScriptでは、これを移植したjs-tiktokenライブラリを利用できます。

github.com

js-tiktokenのインストール

npmやpnpmなどのパッケージマネージャーを使って簡単にインストールできます。

# npmの場合
npm install js-tiktoken

# pnpmの場合
pnpm add js-tiktoken

基本的な使い方

js-tiktokenを使ったトークン数計算の基本的な使い方は非常にシンプルです。以下のように実装できます:

import { Tiktoken } from "js-tiktoken/lite";
import o200k_base from "js-tiktoken/ranks/o200k_base";

// Tiktokenのインスタンスを作成
const tiktoken = new Tiktoken(o200k_base);

// テキストからトークン数を計算
const text = "Hello, world!";
const tokens = tiktoken.encode(text);
console.log(`トークン数: ${tokens.length}`);

コードの概要はは以下の通りです。

  1. js-tiktoken/liteからTiktokenクラスをインポート
  2. js-tiktoken/ranks/o200k_baseからトークナイザーモデルをインポート
  3. 計算したいテキストをencode()メソッドで変換し、その結果の配列の長さがトークン数になる

詳細な使い方は、READMEを参照してください。

github.com

トークナイザーの種類

js-tiktokenでは、OpenAIの各モデルで使用されている様々なトークナイザーをサポートしています。代表的なものは以下の通りです:

  • p50k_base - text-davinci-003などで使用
  • cl100k_base - GPT-4およびGPT-3.5-Turboで使用
  • o200k_base - gpt-4o系で使用

使用するモデルに応じて適切なトークナイザーを選択することで、より正確なトークン数の見積もりが可能になります。

FYI)各モデルで使用しているトークナイザーのリスト

github.com

まとめ

js-tiktokenを使うことで、OpenAIのAPIを利用するフロントエンドアプリケーションにおいて、トークン数を正確に見積もることができます。これにより、APIの制限に対する対策やコスト計算が楽になりそうですね👀✨

参考リンク

UnoCSS resetパッケージでTailwind CSS Preflightを導入して未来のTailwind採用の余白を持たせるメモ📝

プロジェクトの初期段階でTailwind CSSを導入するかどうか迷うことがあると思いますが、Tailwindのリセットスタイル(Preflight)だけを取り入れておくと今後Tailwindを導入したくなった際にもスムーズに導入できて良いかなと思い、個人的に最近UnoCSS resetパッケージを使ってTailwind CSSのPreflightをリセットCSSを利用することが多いので導入方法とかをメモ📝

UnoCSS resetとは?

UnoCSSはAtomicなCSSエンジンで、Tailwind CSSと互換性のあるユーティリティクラスを提供しています。UnoCSS自体を使わなくても、@unocss/resetパッケージを単独で使うことができ、これにはTailwind CSS Preflightも含まれています。

github.com

導入方法

パッケージのインストール

まずは@unocss/resetをインストールします。

npm install @unocss/reset

package.jsonに追加されるのはこんな感じです:

{
  "dependencies": {
    "@unocss/reset": "^66.0.0"
    // その他の依存関係
  }
}

リセットCSSの導入

CSSファイル(reset.cssなど)を作成し、次のようにインポートします:

@import "@unocss/reset/tailwind.css";

たったこれだけで、Tailwind CSS Preflightと同等のリセットスタイルが適用されます。シンプルですね! ✨

なぜTailwind Preflightだけを導入するのか?

Tailwindを採用していなくてもPreflightはリセットCSSとしていい感じに思っていて、

tailwindcss.com

そもそも元になっている modern-normalize もアクティブにメンテされておりよく使われているリセットCSSです。

github.com

現状使う想定はあまりないがTailwindの採用の余白を残しておきたいという場合に、 Preflightをすでに使っていれば、スタイルの一貫性を保ったまま移行できる点が良いかなと感じています。

Nuxt UI + pnpm環境で`tailwindcss`が解決できない問題の解決方法メモ📝

Nuxt UIをpnpm環境で導入した際、tailwindcssが解決できずにエラーとなる事象に遭遇したので、その解決方法をメモ📝

発生したエラー

Nuxt UIの公式ドキュメント通りにセットアップしたものの、下記のようなエラーが発生。

ERROR  Internal server error: Can't resolve 'tailwindcss' in 'path/file'

tailwindcss自体は依存に含まれているはずなのに、Nuxtのビルド時に解決できずに落ちてしまう👀

原因

pnpmはデフォルトで依存パッケージをフラットに配置せず、各パッケージごとにnode_modulesを分離して管理する。そのため、Nuxt UIやtailwindcssのような依存解決が深いパッケージの場合、ルートに依存が見つからずエラーになることがある。

解決方法

pnpm-workspace.yaml.npmrcに下記の設定を追加することで、依存パッケージをルートにフラットに配置でき、エラーが解消された。

# .npmrc
+ shamefully-hoist=true

この設定により、node_modulesがnpmのようなフラットな構造になり、Nuxt UIやtailwindcssの依存解決がうまくいくようになる。

公式ドキュメントにも以下の記載があった😭

If you're using pnpm, ensure that you either set shamefully-hoist=true in your .npmrc file or install tailwindcss in your project's root directory. Installation - Nuxt UI

公式ガイドの注意事項はちゃんと読まないとダメですね・・・!

個人開発のvite pluginのバンドラーをrollupからrolldownへ移行してみたので手順をメモ📝

これまでTypeScript製のライブラリ開発でRollupを使っていたが、最近話題のrolldownへビルドツールを移行したので、その手順やハマりどころをメモしておく。

変更内容の概要

移行にあたって主に以下のようなことを実施した。

  • Rollup関連の依存パッケージ(@rollup/plugin-commonjsrollup-plugin-dtsなど)を削除し、rolldownrolldown-plugin-dtsに置き換えた。
  • rollup.config.jsを削除し、rolldown.config.tsを新規作成。
  • package.jsonのbuildスクリプトrolldown --config rolldown.config.tsに変更。
  • pnpm-lock.yamlも依存関係の変更に伴い更新。

差分の全量に関しては以下のcommitを参照してください。

github.com

変更後のrolldown.config.ts

-import commonjs from "@rollup/plugin-commonjs";
-import replace from "@rollup/plugin-replace";
-import typescript from "@rollup/plugin-typescript";
-import { dts } from "rollup-plugin-dts";
+import { defineConfig } from "rolldown";
+import { dts } from "rolldown-plugin-dts";
 
-export default [
+export default defineConfig([
   {
     input: "src/index.ts",
     external: ["node:fs", "node:path"],
+    define: {
+      "import.meta.vitest": "undefined",
+    },
     output: [
       {
         format: "es",
         exports: "auto",
       },
     ],
-    plugins: [
-      replace({
-        "import.meta.vitest": "undefined",
-        preventAssignment: true,
-      }),
-      typescript(),
-      commonjs(),
-    ],
   },
   {
     input: "src/index.ts",
-    output: [{ format: "es", file: "dist/index.d.ts" }],
-    plugins: [dts()],
+    external: ["node:fs", "node:path"],
+    output: [{ format: "es", dir: "dist" }],
+    plugins: [dts({ emitDtsOnly: true })],
   },
-];

所感

公式ドキュメントもまだ発展途上な印象があるが、基本的なビルド用途ならすぐに移行でき、rollupに比べて標準で色々サポートされているのでpackage.jsonもシンプルに出来る点は良いなと思った⚡️

小さなライブラリなので、そこまで大きな変化は無いが移行前後でビルド速度が速くなった🐰✨

before

$ rollup --config

src/index.ts → dist/index.esm.js, dist/index.umd.cjs...
created dist/index.esm.js, dist/index.umd.cjs in 1.7s

src/index.ts → dist/index.d.ts...
created dist/index.d.ts in 678ms

after

$ rolldown --config rolldown.config.ts

[log] <DIR>/index.esm.js.map  asset │ size: 12.08 kB
[log] <DIR>/index.esm.js      chunk │ size:  5.09 kB
[log] <DIR>/index.umd.cjs.map  asset │ size: 12.11 kB
[log] <DIR>/index.umd.cjs      chunk │ size:  6.15 kB
[log] <DIR>/index.d.ts  chunk │ size: 0.51 kB
[log]
[success] Finished in 1.18 s

参考

rolldown.rs

github.com

`@property`を利用したモダンなCSS変数管理メモ📝

CSSカスタムプロパティ(CSS変数)はすでに多くの開発者に利用されていますが、個人のサービスは@propertyという、より高度な変数管理を行えるCSS at-ruleを使ってみたので使い方をメモ📝

@propertyとは

@propertyCSS Properties and Values API Level 1の一部として導入された仕様で、カスタムプロパティに型情報や初期値、継承の振る舞いなどを設定できます。

@property --my-color {
  syntax: '<color>';
  inherits: false;
  initial-value: #c0ffee;
}

developer.mozilla.org

ブラウザサポート状況

2025年4月現在、@propertyは主要なブラウザでサポートされています

https://developer.mozilla.org/ja/docs/Web/CSS/@property#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7

@propertyの基本構文

@propertyの基本構文は以下の通りです:

@property --custom-property-name {
  syntax: '<構文>';
  inherits: true | false;
  initial-value: <初期値>;
}
  • syntax: 値の型を指定(<color><length><number>など)
  • inherits: 値を子要素に継承するかどうか
  • initial-value: 初期値(syntaxに準拠した値である必要あり)

TIPS

不正なproperty指定の挙動

@propertyを使う際には正しい構文に従って記述する必要があります。不正な指定をした場合は無効となり無視されます。

--item-size の値は 1000px に設定されます。 1000px は > <length> の値ですが、 @property 宣言では <percentage> であることが要求されているため、この宣言は無効であり無視され、すなわち親に設定されている継承可能な 20% が使用されます。 https://developer.mozilla.org/ja/docs/Web/CSS/@property#%E4%BE%8B

ChromeではConsoleの「問題」に以下のようなエラーが表示されて無効となるようです。

Ignored @property rule An @property rule is ignored because it contains an invalid property or is missing a required one: initial-value: 1.25rem

初期値に関する制約

@propertyを使う上で特に注意すべき点として、初期値は計算上独立している必要があるということがあります。これはW3Cの仕様書でも明確に述べられています。

The 'initial-value' must be computationally independent. CSS Properties and Values API Level 1

つまり、rememなどの相対的な単位を初期値として直接使用することができません。例えば:

/* ❌ 以下は動作しません */
@property --my-space {
  syntax: '<length>';
  inherits: false;
  initial-value: 1rem; /* 相対値なので使用できない */
}

この制約を回避するために、別のカスタムプロパティを経由して相対値を参照するようにしました。

@property --my-space {
  syntax: '<length>';
  inherits: false;
  initial-value: 16px; /* 絶対値を使用 */
}

:root {
  --space-base: 1rem;
}

まとめ

@propertyを使ったCSS変数管理は、やはり初期値としてフォールバックの値が設定できるので不正な値設定を抑制できるのと、syntaxの指定によりブラウザ上で不正な値にも気づきやすいといったメリットがあって良いなと感じました。ただし、初期値に相対単位を直接使えないなどの制約もあるため仕様をよく理解して利用したい📝

参考リンク

coliss.com

Nuxt Content で外部プロジェクトのファイルを取得する方法メモ📝

Nuxt Content は Nuxt.js アプリケーションでコンテンツを管理するための優れたモジュールですが、デフォルトでは現在のプロジェクト内のファイルしか参照できません。しかし、プロジェクト外のファイルを取得したいケースもあります。

今回はそんなときに cwd オプションを利用すると実現できたので利用方法をメモ📝

外部ファイルを取得するための問題

通常、Nuxt Content は content ディレクトリ内のファイルを自動的に解析して提供します。 モノレポ構成で別パッケージのドキュメントを表示したいというような外部ファイルを参照したいケースでは単純に../other_project/contentsというように辿ってもコンテンツを扱うことはできません。

解決策: cwd オプションに絶対パスを指定する

Nuxt Content の defineCollection 関数内で source オブジェクトの cwd プロパティを設定することで、ファイルの取得元ディレクトリを指定できます。Nuxt Content の公式ドキュメントにも明記されているように、cwd プロパティには必ず絶対パスを設定する必要があります。

cwd
If you want to include files from a folder outside the content directory, set the absolute path of that folder to the cwd property. https://content.nuxt.com/docs/collections/sources#cwd

サンプルコード

以下の content.config.ts は、プロジェクト外の .github フォルダ内の Markdown ファイルを取得する例です:

import { defineContentConfig, defineCollection } from "@nuxt/content";
import path from "node:path";

// 絶対パスを生成して外部ディレクトリを指定
const externalDocsPath = path.resolve("../other_project", "contents");

export default defineContentConfig({
  collections: {
    externalDocs: defineCollection({
      type: "page",
      source: {
        // cwd: 絶対パスで取得元のベースディレクトリを指定
        cwd: externalDocsPath,
        include: "**/*.md"
      }
    }),
  },
});

参考リンク

content.nuxt.com

nodejs.org