🦁

Nuxt3のAuto-imports機能 (components/composables) を拡張する

2023/09/29に公開1

はじめに

Nuxt3 には Auto-imports 機能というありがたい機能があります。/components/composables直下のファイルや Vue3 API であるrefcomputedなどが import 不要で利用でき非常に便利です。

下記記事では「Pinia で Auto-imports できないか?」という点で調査を行いました。

https://zenn.dev/gangannikki/articles/pinia-autoimports

本記事では機能のベースになっているunjs/unimportの README も見ながらcomponentsimportsオプションを拡張していきます。

実際の利用ケースを考えながら拡張していきます。

対象ユーザー

  • nuxt.config.tscomponentsimportsオプションを拡張したい人
  • Vitest、Storybook で利用するunplugin-auto-importの設定を拡張したい人
  • unjs/unimportを眺めたい人

本記事はタイトルに Nuxt3 と書いてますが半分くらいはunjs/unimport に関する内容です。そのため、nuxt.config.tsのみならずvite.config.tsvitest.config.tsを拡張したいケースでも利用できるかと思います。

Nuxt3 の Auto-imports とは

Auto-imports 機能の説明は公式ドキュメントが一番分かりやすいです。まずはご一読頂けると良いかと思います(この記事を眺めてる時点で n 回は読んでる方だと思いますが)。

https://nuxt.com/docs/guide/concepts/auto-imports

https://nuxt.com/docs/guide/directory-structure/components

components 設定を拡張する

下記で紹介しない設定値、詳細設定については公式ドキュメント、公式 GitHub を参照ください。

https://github.com/nuxt/nuxt/blob/3fd4f6051fe30bd7c2307ba757acd64766c79d3c/packages/schema/src/types/components.ts

デフォルトで利用できるもの

デフォルトで以下が設定されています。

  • /componentsディレクトリの Auto-imports
  • /componentsディレクトリの Nested scan

例えば、実際の利用ケースでは下記のようなネストしたディレクトリになるかと思います。

| components/
--| base/
----| foo/
------| Button.vue
--| ui/
----| Input.vue

これは Template で呼び出す際に以下のような命名になります。

<template>
  <div>
    <BaseFooButton />
    <UiInput />
  </div>
</template>

case1: pathPrefix を無効にする

デフォルトの pathPrefix の付け方は非常に便利ではあるものの、ネストが深くなった時に可読性が下がる要因になります。
例えば、以下のようなネスト構造です。

| components/
--| product1/
----| base/
------| buttons
--------| BaseButton.vue

この場合非常に長い命名となります。非常に煩雑な名前となっており不必要な情報が混じっています。

<template>
  <!-- Product1 や Buttons は含めなくても良い -->
  <!-- Baseが重複している -->
  <Product1BaseButtonsBaseButton />
</template>

本件の解決策はCustom directoriesに記載されています。

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: "~/components",
+     pathPrefix: false
    },
  ],
});
利用イメージは以下のようになります。
<template>
  <!-- Auto-imports  /components/product1/base/buttons/BaseButton.vue -->
  <BaseButton />
</template>

case2: 任意の pathPrefix を設定する

case1 の設定では全てのpathPrefixを無効にしました。case2 では任意の pathPrefix を設定したい場合を想定していきます。
例えば、以下のように設定したい場合です。

<template>
  <!-- Auto-imports /components/ui/button.vue -->
  <UiButton />
</template>

v0.devshadcn/uiでは@/components/ui/*というディレクトリ構造をしており、コピペしたコンポーネント / プロダクト固有のコンポーネントを分けたい というユースケースは徐々に増えていくと思われます。

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      // eg: /components/ui/Button.vue -> <UiButton />
      // eg: /components/ui/Select/index.vue -> <UiSelect />
      path: "~/components/ui",
+     prefix: "Ui"
    },
  ],
});

本設定方法はshadcn-vueの Nuxt チュートリアルにも書かれています。

https://www.shadcn-vue.com/docs/installation/nuxt.html

case3: 特定のファイルだけを含めたい

componentsオプションで設定した内容は.nuxt/components.d.tsファイルにエクスポートされます。
以下のようなディレクトリ構造の場合、コンポーネントではないtype.tsprops.ts、起動時に Nuxt Config を経由しないindex.stories.tsは含めたくないですよね。

| components/
--| ui/
----| button/
------| index.vue
------| type.ts
------| props.ts
------| index.stories.ts : Story ファイル
------| schema.ts : zod / yup スキーマ

コンポーネントではないtype.tsprops.ts.nuxt/imports.d.tsファイルから参照する方が責務的に自然だと思います。そこでextensionsオプションを用いて.vueファイルのみを含めるようにします。

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: "~/components/ui",
+     extensions: [".vue"],
    },
  ],
});

公式ドキュメントではComponent extensionsに記載があります。

case4: 特定のファイルを除外したい

case3 の場合、Storybook の*.stories.tsは除外できたものの、Histoire の*.story.vueは除外できません。本件は公式ドキュメントに解決方法が記載されていなかったため、unimportの docs を眺めてみます。

するとignoreというオプションが確認できました。

The ignore option is used to filter out the exports, it can be a string, regex or a function that returns a boolean.

https://github.com/unjs/unimport/tree/main#exports-auto-scan

こちらを用いると除外できるようですね。

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: "~/components/ui",
      extensions: [".vue"],
+     ignore: ["**/*.story.vue"],
    },
  ],
});

(そもそもignoreオプションだけでも良さそう)。

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: "~/components/ui",
-     extensions: [".vue"],
+     ignore: ["**/*.story.vue", "**/*.stories.ts", "**/*.ts"],
    },
  ],
});

imports 設定を拡張する

こちらについても下記で紹介しない設定値、詳細設定については公式ドキュメント、公式 GitHub を参照ください。

https://github.com/nuxt/nuxt/blob/3fd4f6051fe30bd7c2307ba757acd64766c79d3c/packages/schema/src/types/imports.ts

デフォルトで利用できるもの

以下はデフォルトで設定されている内容です。

  • /composablesディレクトリの Auto-imports
  • /utilsディレクトリの Auto-imports

case1: Built-in Presets 導入

Nuxt3 の Auto-imports は unjs/unimport をベースにしているため unjs/unimport が提供する presets を利用できます。

https://github.com/unjs/unimport/tree/main#built-in-presets

nuxt.config.ts
export default defineNuxtConfig({
  imports: {
+   presets: ["@vueuse/core", "pinia"],
  },
});

case2: Custom Presets

では、ライブラリ側で提供していないものはどうすればいいのか。よく利用するものだとzodyupなどですかね。
こちらについては Nuxt3 公式のAuto-import from third-party packagesに明記されています。

nuxt.config.ts
export default defineNuxtConfig({
  imports: {
+   presets: [
+     {
+       from: "zod",
+       imports: ["string", "object"],
+     }
+   ],
  },
});

unimport 側はこちらです。
https://github.com/unjs/unimport/tree/main#custom-presets

case3: 特定ディレクトリを含める

Pinia/Vuex を利用する場合は/stores、スキーマや type ファイルを 1 箇所でまとめたい場合は/schemas/typesなど、ディレクトリレイヤーを増やす運用ケースがあるかと思います。しかし、悲しいことにこのディレクトリが Auto Imports されることはありません。

特定ディレクトリを含めたい場合imports.dirsオプションを利用します。

nuxt.config.ts
export default defineNuxtConfig({
  imports: {
+   dirs: ["stores", "schemas"],
  },
});

Nuxt3 公式の/composables#How Files Are Scannedに記載があります。

case4: 特定ファイルを含める

case3 のケースはディレクトリを増やす場合のユースケースでした。しかし、プロダクトによっては関心ごとを 1 つのディレクトリにまとめることがあると思います。例えば、components 設定 case3: 特定のファイルだけを含めたいのようなケースです。

| components/
--| ui/
----| button/
------| index.vue
------| type.ts
------| props.ts
------| schema.ts : zod / yup スキーマ

components 設定 case3: 特定のファイルだけを含めたいの設定済みであれば components/ui/button/index.vue.nuxt/components.d.tsに export 済みです。本ケースで対象にしたいのはそれ以外のtype.tsprops.tsschema.tsなどです。

本ケースは下記の設定で大丈夫そうでした。

nuxt.config.ts
export default defineNuxtConfig({
  imports: {
   dirs: [
    "stores",
    "schemas",
+   "components/ui/*"
  ],
  },
});


「なぜこれでいけるんだろう?」と思い unimport の GitHub を眺めました。するとScanDirExportsOptions["filePatterns"]というオプションを見つけました。auto-import のスキャン対象として*.{ts,js,mjs,cjs,mts,cts}がデフォルト定義されていました。これのおかげですね。

https://github.com/unjs/unimport/blob/5f7e56f72fc57ae02a21cc497a27b0a2cb6d98ee/src/types.ts#L198-L204

Q. Vitest や Storybook で利用する場合はどうすればいいのか?

本設定はnuxt.config.tsに設定しており Vitest や Storybook 実行時には反映されません。
解決策として挙げられるのはunplugin/unplugin-auto-importを用いてvite.config.tsvitest.config.tsを拡張することです(Nuxt Config を流用できる方法あれば知りたい)。

vite.config.mts
/// <reference types="vitest" />

import Vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    Vue(),
    AutoImport({
      imports: [
        "vue",
        "vue-router",
        "pinia",
        "vue-i18n",
        { from: "zod", imports: ["string", "object"] },
      ],
      dirs: ["src/stores", "src/schemas"],
      dts: "src/.nuxt/auto-imports.d.ts",
    }),
  ],
});

おわりに

本記事では Nuxt3 の Auto-imports 設定、ベースとなっている unjs/unimportについて調べてみました。
Auto-imports 機能の良いところとして

  • 人によってインポートの仕方・順番が違う
  • インポート漏れをレビューで指摘された

などの本質でない議論・指摘を避けられる点があると思ってます。
本質的ではない点はリンター、フォーマッター、ジェネレーターなど機械的に処理し、本質的な部分に注力して行けたら良いですね。

余談

今回調べてて面白かったのはislandオプションがあったことです。コンポーネント単位でアイランドアーキテクチャの可否を設定できたりするんですかね?
Nuxt におけるアイランドアーキテクチャは Experimental feature のため今後の展開が楽しみです(早く使いたい)。

https://github.com/nuxt/nuxt/blob/3fd4f6051fe30bd7c2307ba757acd64766c79d3c/packages/schema/src/types/components.ts#L75-L78

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion

GANGANGANGAN

{vite,vitest}.config.{ts,mts}においてsrc/の重複が煩わしい場合、resolverを用意しても良いかと思います。

// case1: node:path 利用

vite.config.mts
import { cwd ] from "node:process";
import { resolve } from "node:path"

const resolveRootDir = (path: string): void => resolve(cwd(), "src", path);

export default defineConfig({
  plugins: [
    Vue(),
    AutoImport({
      dirs: [ resolveRootDir("stores"), resolveRootDir("schema")],
      dts: resolveRootDir(".nuxt/auto-imports.d.ts"),
    }),
  ],
});

// case2: nuxt/kitのcreateResolver 利用

createResolver() は内部でpatheを利用してます。

vite.config.mts
import { createResolver } from "nuxt/kit";
const { resolve } = createResolver(import.meta.url);

export default defineConfig({
  plugins: [
    Vue(),
    AutoImport({
      dirs: [ resolve("src", "stores"), resolve("src", "schema")],
      dts: resolve("src", ".nuxt/auto-imports.d.ts"),
    }),
  ],
});