Viteでnpmライブラリを作る方法と、自作したUtilityスクリプト集「umaki」の使い方の宣伝
Viteでnpmライブラリを作る方法
ViteにはLibrary Modeというnpmライブラリなどを作るためのモードが用意されていて、これを用いると一瞬で環境が整います。
まずはyarn create vite
を行い、Others
→create-vite-extra
→library
→TypeScript
を選択してプロジェクトを作成します。
bash-3.2$ yarn create viteyarn create v1.22.22[1/4] 🔍 Resolving packages...[2/4] 🚚 Fetching packages...[3/4] 🔗 Linking dependencies...[4/4] 🔨 Building fresh packages...
success Installed "create-vite@5.5.5" with binaries: - create-vite - cva✔ Project name: … vite-project✔ Select a framework: › Others✔ Select a variant: › create-vite-extra ↗yarn create v1.22.22[1/4] 🔍 Resolving packages...[2/4] 🚚 Fetching packages...[3/4] 🔗 Linking dependencies...[4/4] 🔨 Building fresh packages...
success Installed "create-vite-extra@2.4.1" with binaries: - create-vite-extra✔ Select a template: › library✔ Select a variant: › TypeScript
Scaffolding project in /vite-project...
Done. Now run:
cd vite-project yarn yarn dev
✨ Done in 12.97s.✨ Done in 23.13s.
基本的にはこれで完了です。
あとはひたすらコードを書いていくだけですが、実際に作ってみた際に行った設定などを以下に記します。
1. 型定義ファイルの出力
型定義ファイルはプラグインvite-plugin-dts
を使うことで出力できます。特別な設定は不要です。
import { defineConfig } from 'vite'import dts from 'vite-plugin-dts'
export default defineConfig(({ mode }) => { return { plugins: [ dts() ], ... }})
2. Productionビルド時にログを出力しないようにする
ライブラリとして公開する際にはProductionビルド時にログを出力しないようにすることが望ましいはずですので、dropするように設定しました。
import { defineConfig } from 'vite'import dts from 'vite-plugin-dts'
// isProduction const isProduction = process.env.NODE_ENV === 'production'
export default defineConfig(({ mode }) => { return { plugins: [ dts() ], esbuild: isProduction ? { drop: ['debugger'], pure: [ 'console.log', 'console.info', 'console.table', 'console.time', 'console.timeEnd', 'console.trace' ] } : {}, }})
3. ビルドを`esm`と`cjs`の両方で出力する
ライブラリを利用する側が好きな形式で利用できるよう、ES形式とCommonJS形式の両方でビルドを出力するように設定しました。
設定自体はbuild.lib.formats
に配列で["es", "cjs"]
という様な形で指定することも出来ますが、各フォーマットの設定を詳細に指定できるようrollup側で設定しています。
import { defineConfig } from 'vite'import dts from 'vite-plugin-dts'
// isProductionconst isProduction = process.env.NODE_ENV === 'production'
export default defineConfig(({ mode }) => { return { plugins: [ dts() ], esbuild: isProduction ? { drop: ['debugger'], pure: [ 'console.log', 'console.info', 'console.table', 'console.time', 'console.timeEnd', 'console.trace' ] } : {}, build: { lib: { entry: './src/index.ts', name: 'foobar', fileName: (format) => `index.${format}.js` }, rollupOptions: { output: [ { format: 'es', preserveModules: true, preserveModulesRoot: 'src', entryFileNames: ({ name: fileName }) => { return `${fileName}.es.js` }, exports: 'named', }, { format: 'cjs', preserveModules: true, preserveModulesRoot: 'src', entryFileNames: ({ name: fileName }) => { return `${fileName}.cjs.js` }, exports: 'named', } ] } } }})
preserveModules
をtrue
にすることでモジュールツリー構造を保持することができるようになるので、ここはtrue
にしておくと良いでしょう。これによりツリーシェイキングも効くようになります。
また、preserveModulesRoot
はpreserveModules
がtrue
の場合に指定することで、モジュールツリーのルートディレクトリを指定できます。
上記の例ではsrc
ディレクトリをルートディレクトリとして指定しているので、出力ディレクトリのdist
ディレクトリ内にsrc
ディレクトリの構造を保ったまま出力されるようになります。
参考: バンドルサイズに優しい tree shakeable なライブラリを作成する
4. licenseファイルを出力する
ライブラリを実際に公開する際にはLICENSE
ファイルを含めることが望ましいはずですので、rollup-plugin-license
を用いて出力するようにしました。
import * as path from 'node:path' import license from 'rollup-plugin-license'import { defineConfig } from 'vite'import dts from 'vite-plugin-dts' import packageJson from './package.json'
// isProductionconst isProduction = process.env.NODE_ENV === 'production'
// banner const banner = `/*! * name: ${packageJson.name} * version: v${packageJson.version} * description: ${packageJson.description} * author: ${packageJson.author} * homepage: ${packageJson.homepage} * Released under the ${packageJson.license} License * * This software includes dependencies licensed under MIT, ISC, BSD-3-Clause, and Apache-2.0. * For license information, please see LICENSE.txt */ `
export default defineConfig(({ mode }) => { return { plugins: [ dts(), license({ sourcemap: true, thirdParty: { output: path.join(__dirname, 'dist/LICENSE.txt') } }) ], esbuild: isProduction ? { drop: ['debugger'], pure: [ 'console.log', 'console.info', 'console.table', 'console.time', 'console.timeEnd', 'console.trace' ] } : {}, build: { lib: { entry: './src/index.ts', name: 'foobar', fileName: (format) => `index.${format}.js` }, rollupOptions: { output: [ { format: 'es', preserveModules: true, preserveModulesRoot: 'src', entryFileNames: ({ name: fileName }) => { return `${fileName}.es.js` }, exports: 'named', banner }, { format: 'cjs', preserveModules: true, preserveModulesRoot: 'src', entryFileNames: ({ name: fileName }) => { return `${fileName}.cjs.js` }, exports: 'named', banner } ] } } }})
上記だと以下の様なよくみるライセンスのバナーを出力しつつ、dist
ディレクトリ内にLICENSE.txt
ファイルを出力できるようになります。
/*! * name: foobar * version: v0.1.0 * description: hoge. * author: hoge * homepage: https://github.com/hoge/foobar * Released under the MIT License * * This software includes dependencies licensed under MIT, ISC, BSD-3-Clause, and Apache-2.0. * For license information, please see LICENSE.txt */
5. 作成するライブラリ内に別のnpmライブラリをインポートしている場合に、そのライブラリを外部依存関係に指定する
例えばdayjs
をライブラリ中でimportしている場合は、build.rollupOptions.external
にdayjs
を指定しておくことでdayjs
を外部依存関係に指定することができます。
...省略export default defineConfig(({ mode }) => { return { ...省略 build: { ...省略 rollupOptions: { ...省略 external: [ 'dayjs' ], } } }})
6. ライブラリのテストを行う
テストにはvitest
を用いました。DOM操作が必要な場合はhappy-dom
もインストールしておきます。
yarn add -D vitest happy-dom
vite.config.ts
に以下の設定を追加します。これだけです。(便利すぎ)
...省略import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => { return { ...省略 test: { globals: true, environment: 'happy-dom', include: ['src/libs/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] }, ...省略 }})
自作したUtilityスクリプト集「umaki」の使い方と宣伝
最後に上記流れで実際に作成し公開したUtilityスクリプト集「umaki」の使い方と宣伝です。
Web制作をしている時にあると便利なUtilityスクリプトをまとめたもので、以下の様な形で使うことができます。
yarn add umaki
import { debounce } from 'umaki'
const debouncedFunction = debounce(() => { console.log('Debounced!')}, 300)
window.addEventListener('resize', debouncedFunction)
import { throttle } from 'umaki'
const throttledFunction = throttle(() => { console.log('Throttled!')}, 300)
window.addEventListener('scroll', throttledFunction)
import { wrapTextWithSpans } from 'umaki'
const element = document.createElement('div')element.textContent = 'hello'wrapTextWithSpans(element)console.log(element.innerHTML) // '<span>h</span><span>e</span><span>l</span><span>l</span><span>o</span>'
import { waitForAllMediaLoaded } from 'umaki'
;(async () => { const allMediaLoaded = await waitForAllMediaLoaded() console.log(allMediaLoaded) // true or false
// first view only const firstViewMediaLoaded = await waitForAllMediaLoaded(true) console.log(firstViewMediaLoaded) // true or false})()
その他にも幾つかありますので、詳しくは以下のリンクからご覧ください。TypeScriptに対応済みです。
- npm: https://www.npmjs.com/package/umaki
- Repository: https://github.com/TsubasaHiga/umaki