Closed7

Vite + Vue 3 のプロジェクトをライブラリモードで出力してパッケージ化するまでにしたことメモ

七瀬七瀬

Vite+Vue+Vuetifyを使ったプロジェクトを複数制作しており、規模が大きくなるにつれて共通処理や共通コンポーネントが増えてきた。ライブラリにして共有できるようにしようと思ってパッケージ化してみた。

公式でViteのライブラリモードのドキュメントはあるが、実例があまり見つからなかったので解決に時間がかかってしまった。それはその解決までのメモ。

前提条件

  • Vue: 3.5.3
  • Vuetify: 3.7.1
  • Vite: 5.4.2
  • Vitest: 2.0.5

その他の依存パッケージについては必ずしも必須ではないので出てきた時点で後述する。

七瀬七瀬

ディレクトリ構成

自作したパッケージでは以下のようなファイル構成になっている。

|-- dist
|-- src
|   |-- components
|   |   |-- index.ts
|   |   |-- AnimatedClock.vue
|   |   |-- AppBase.vue
|   |   `-- ThemeToggleButton.vue
|   `-- lib
|       |-- use
|       |   |-- index.ts
|       |   |-- computedJSON.ts
|       |   `-- useIntervalFnWithPauser.ts
|       |-- array.ts
|       `-- (省略)
|-- test
|   |-- components
|   |   `-- (省略)
|   `-- lib
|       `-- (省略)
|-- package.json
|-- tsconfig.json
|-- tsconfig.node.json
|-- vite.config.ts
`-- yarn.lock

./src 以下がライブラリとなるファイル群で、その中に components/lib/ がある。前者には Vue + Vuetify を使ったコンポーネントがあり、後者はそれ以外のプログラムだ。コンポーネントが lib/ のライブラリを使うことはありえるが、その逆はない。

./test 以下にも components/lib/ があり、./src とだいたい対応している。

七瀬七瀬

ゴール

上記の構成のパッケージを、以下のように import したい。

import { mergeArrayBy } from '@nanase/alnilam/array';
import { computedJSON } from '@nanase/alnilam/use';

import { AppBase, AnimatedClock } from '@nanase/alnilam/components';

./lib 以下にあるものはモジュールを細分化して export された関数や定数が衝突しないようにしたい。コンポーネントについてはコンポーネント名が衝突するとは思えないので、ひとつのモジュールで公開したい。今後数が増えたら細分化するかもしれないが、その手順は ./lib でやることと変わらないはずである。

以下、どうやって設定を作っていくかを見ていく。

七瀬七瀬

tsconfig.json

Viteでビルドをするのであまり効果はないと思われるが、VSCodeで開発する際にこの設定が読み取られるので一旦この設定でやる。ポイントは paths, types, 'include' あたり。

paths で import をしたとき(外部からパッケージを使うときとは別の話)に 'src/lib/array' と打たねばならないところを '@/lib/array' と記述できる。どこまでディレクトリが深くても @ から始まれば ./src を指すので、絶対パス指定のような感覚で使える。ビルドが通るのにVSCode上の警告でモジュールが見つからない際はここをチェックすればいい。

types で Vitest のテスト用関数を import なしで使うことができるが、実際には後述の vite.config.ts にも設定が必要になる。

include にはコンパイルの対象になるファイルのパターンを入れる。env.d.ts を入れる場合はそれも必要。コンポーネントも対象になるので src/**/*.vue も入れている。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    "lib": ["es2022", "DOM", "DOM.Iterable"],
    "allowJs": false,
    "jsx": "preserve",
    "emitDeclarationOnly": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "downlevelIteration": true,
    "skipLibCheck": false,
    "noErrorTruncation": true,

    "strict": true,

    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,

    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },

    "types": ["vitest/globals"]
  },
  "exclude": ["node_modules"],
  "include": ["src/**/*", "src/**/*.vue", "test/**/*"],
  "vueCompilerOptions": {
    "jsxSlots": true
  }
}
七瀬七瀬

tsconfig.node.json

ビルド時の型チェック時に使う設定。こちらにも paths の設定は入れておく。vite.config.ts自体のチェックも行うので include に追加しておく。

{
  "extends": "@vue/tsconfig/tsconfig.json",
  "include": [
    "src/**/*",
    "src/**/*.vue",
    "test/**/*",
    "test/**/*.vue",
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "playwright.config.*"
  ],
  "compilerOptions": {
    "composite": true,
    "types": ["node"],
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
七瀬七瀬

vite.config.ts

最終的に採用した vite.config.ts は以下の通り。

/// <reference types="vitest" />
import { resolve } from 'path';
import { defineConfig } from 'vite';
import Vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts';
import { libInjectCss } from 'vite-plugin-lib-inject-css';

const root = resolve(__dirname);
const srcDir = resolve(root, 'src');

export default defineConfig({
  plugins: [
    Vue(),
    dts({
      exclude: ['test/**/*.ts', '**/*.test.ts'],
    }),
    libInjectCss(),
  ],
  test: {
    root,
    include: ['test/**/*.test.ts'],
    globals: true,
    coverage: {
      reporter: ['text', 'json'],
      include: ['src/**/*.{ts,vue}'],
      exclude: ['**/index.ts'],
    },
    server: {
      deps: {
        inline: ['vuetify'],
      },
    },
  },
  resolve: {
    alias: [{ find: '@', replacement: srcDir }],
  },
  build: {
    lib: {
      entry: {
        'components/index': resolve(srcDir, 'components', 'index.ts'),
        'lib/use/index': resolve(srcDir, 'lib', 'use', 'index.ts'),
        'lib/array': resolve(srcDir, 'lib', 'array.ts'),
      },
      name: 'alnilam',
      formats: ['es'],
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
        chunkFileNames: 'chunks/[name].[hash].js',
        assetFileNames: 'assets/[name][extname]',
      },
    },
    outDir: resolve(root, './dist'),
    emptyOutDir: true,
  },
});

plugins

最低限必要なのは3つ。

yarn add -D @vitejs/plugin-vue vite-plugin-dts vite-plugin-lib-inject-css

@vitejs/plugin-vueについてはVueをViteで扱っていれば必ず使っているはずなので解説しない。

vite-plugin-dts.ts.vue から型定義ファイルを作るためのプラグイン。ライブラリモードでのみ有効。公開する型からのみ作られるのでテストコードから作る必要はなく、./test 以下と *.test.ts のファイルは除外している。

vite-plugin-lib-inject-cssはコンポーネント内で使われているCSSを、コンポーネントを import すれば自動でCSSも import されるように加工してくれるプラグイン。ライブラリモードでのみ有効。このプラグインを用いない場合は dist/style.css にCSSがまとめられて出力される。後述の assetFileNames も指定することで dist/assets/style.css に出力される。

test

Vitest で使う設定。冒頭の /// <reference types="vitest" />testdefineConfig に渡せるようになる。globals: true を指定することで Vitest のテスト用関数がグローバル関数として import なしに使えるようになる。

詳細は省略。

resolve

tsconfig.json で設定したものと同じく、ライブラリ内の import@ から始まるものを ./src で置き換えられるようにするためのもの。

build

これがライブラリモードの設定本体。build.lib が存在することでライブラリモードになる。

build.lib.entry

ライブラリのエントリポイント。複数設定ができるが、マルチエントリポイントは対応している形式が限られているので build.lib.formats で限定する必要がある(未指定だとエラーになる)。

エントリポイント名をキーに、エントリポイント先のパスを値として与える。index.ts をエントリポイントにする場合は **/index の形式にする必要がある。

// ✅️ 正しい例
'components/index': resolve(srcDir, 'components', 'index.ts'),

// ❌️ うまくいかない
'components/': resolve(srcDir, 'components', 'index.ts'),

// ❌️ うまくいかない
'components/index': resolve(srcDir, 'components'),

このエントリポイントの設定は後述の package.json と対応させる必要がある。

build.rollupOptions

build.rollupOptions.externalを指定することでビルドに含めない外部パッケージを明示的に指定できる。たとえば Vue は依存パッケージとして要求されるのでビルドに含める必要がない。ちなみにこの指定は正規表現を使うことができる。

build.rollupOptions.output.globalsはグローバル変数がどのモジュールと対応しているかを指示する設定。ここで、グローバル変数 Vuevue モジュールと対応していることを指定している。

build.rollupOptions.output.chunkFileNamesassetFileNames と似ているが、こちらはチャンクファイル用。エントリポイントに指定された .ts ファイルは対応する .js にコンパイルされるが、それ以外のファイルはチャンクファイルとして扱われる。指定をしないと ./dist 以下に作られるがこの chunkFileNames を指定することで任意のファイルパスに変更できる。今回は chunks/ と先頭につけているのでチャンクファイルはすべて ./dist/chunks/ 以下に生成される。

build.outDir

出力先ディレクトリ。今回は ./dist に指定。

build.emptyOutDir

ビルド時、出力先ディレクトリに存在しているファイルを全部削除してからビルド後の生成物を作るかを指定できる。ビルドの都度削除→生成が発生するが、私の場合は出力先ディレクトリも git で管理しているので true にして全削除している。

七瀬七瀬

package.json

全てを書き出すと長いので、必要部分のみ掲載する。

{
  "name": "@nanase/alnilam",
  "version": "0.4.4",
  "private": true,
  "type": "module",
  "files": [
    "dist"
  ],
  "exports": {
    "./components": {
      "types": "./dist/components/index.d.ts",
      "import": "./dist/components/index.js"
    },
    "./use": {
      "types": "./dist/lib/use/index.d.ts",
      "import": "./dist/lib/use/index.js"
    },
    "./array": {
      "types": "./dist/lib/array.d.ts",
      "import": "./dist/lib/array.js"
    },
  },
  "scripts": {
    "build": "run-p type-check build-only",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit -p tsconfig.node.json",
    "test": "vitest",
    "coverage": "vitest run --coverage"
  },

  ...
}

files

パッケージが外部のプロジェクトでインストールされたとき、どのファイルをコピーすればよいかの指定。今回の場合は ./dist にビルドされたものがすべて存在しているので、dist を指定した。

この項目がない場合はパッケージのインストール時にソースファイルを含めた全てのファイルがコピーされる。詳しく解説している記事もあるので参照のこと。

exports

外部のプロジェクトから import したとき、どのようにモジュールが見え、どのファイルを読み込めばいいのかを指定できる。

たとえば

"./components": {
  "types": "./dist/components/index.d.ts",
  "import": "./dist/components/index.js"
}

の記述により、

import { ... } from '@nanase/alnilam/components'

の import が実現される。このとき外部プロジェクトは types に指定された型定義ファイルを参照して型を特定し、import に指定された js ファイルで実行ができるようになる。

今回のようにマルチエントリポイントではなく、ビルド形式に ['es', 'umd'] を指定した場合は *.umd.js というUMD (Universal Module Definition) パターンに従ったファイル形式が出力される。この場合は require で読み込むことになるので、

 "./components": {
   "types": "./dist/components/index.d.ts",
   "import": "./dist/components/index.js",
+  "require": "./dist/componets/index.umd.js"
 }

という指定が増える。

なお、この package.json の設定と vite.config.ts のエントリポイントの設定は対応している必要がある上、ビルド時に両者の記述が一致しているかは自動では検証されない。不足がある場合は外部プロジェクトからの import 時または実行時にエラーとなる。

scripts

Vite でのビルド方法を指定。type-check で tsconfig.node.json を使って型のチェックを行っている。

このスクラップは3ヶ月前にクローズされました