📰

VuePress v2 + Vite のプラグイン・テーマまわりの変更に対応したので記録する

2022/06/19に公開

個人ブログを VuePress v2 + Vite で立ち上げた記事を以前書きました。

その際に npm パッケージとして公開していた VuePress Plugin usePages のリポジトリに Issue が届きました。

https://github.com/monsat/vuepress-plugin-use-pages/issues/1

調べてみると、どうやら VuePress 2.0.0-beta.40 (2022-04-25) にていくつかのブレイクチェンジがあり、プラグイン・テーマの取り扱いが変更されたようです。

備忘録をかね、新たなプラグインの設定方法および今回の対応手順について書いていきたいと思います。

バージョン 2.0.0-beta.40 以降のプラグイン・テーマ

設定ファイルの記述例

docs/.vuepress/config.ts
import { defineUserConfig } from 'vuepress'
import { viteBundler } from 'vuepress'
import { defaultTheme } from 'vuepress'
import { searchPlugin } from '@vuepress/plugin-search'
import { googleAnalyticsPlugin } from '@vuepress/plugin-google-analytics'

export default defineUserConfig({
  bundler: viteBundler(),
  theme: defaultTheme(),
  plugins: [
    searchPlugin(),
    googleAnalyticsPlugin({
      id: 'G-YOUR_ID',
    }),
  ],
})

プラグイン

従来まで config.tsplugins に、文字列でプラグイン名(もしくはプラグインファイルのパス)を記述していました。

今後は上記のようにプラグインをインポートして記述します。
プラグイン固有のオプションは googleAnalyticsPlugin() のようにカッコ内に記述します。

テーマ

テーマも同様です。
インポートしたテーマを、直接 theme プロパティに設定します。
(プロパティが変更になりました)

そのほかいろいろ

デフォルトのバンドラーが Vite になったことに伴い、これまで vuepress-vite で利用していたものはすべて vuepress に置き換え可能です。

バンドラーは bundler で設定するようになったことに加え defineUserConfig() に設定していた型引数は不要になりました。
TypeScript を意識することもなく、どのようなプロパティでどう設定すればよいかが分かりやすいですね。

PluginObject と ThemeObject

プラグインやテーマは、それぞれ PluginObject と ThemeObject を返す関数として記述します。

ThemeObject の作成例

たとえば defaultTheme を継承した childDefaultTheme を作成する場合は、次のように作成可能です。

docs/.vuepress/theme/index.ts
import { defaultTheme } from 'vuepress'
import type { Theme, ThemeObject } from '@vuepress/core'
import { path } from '@vuepress/utils'

export type ChildDefaultThemeOptions = {}

export const childDefaultTheme = (options?: ChildDefaultThemeOptions): Theme => (app): ThemeObject => ({
  name: 'vuepress-theme-default-child',
  extends: defaultTheme(),
  layouts: {
    // レイアウトファイル docs/.vuepress/theme/layouts/Layout.vue を指定
    Layout: path.resolve(__dirname, 'layouts/Layout.vue'),
    // ロゴ docs/.vuepress/public/images/logo.png を指定
    logo: '/images/logo.png',
  },
})

export default childDefaultTheme

PluginObject の作成例

今回変更したプラグインは、次のようになりました。

docs/.vuepress/plugins/vuepress-plugin-use-pages/src/node/index.ts
import type { Page, PluginObject } from '@vuepress/core'

export interface UsePagesPluginOptions {
  startsWith?: string
  filter?: (page: Page) => boolean
  sort?: (a: Page, b: Page) => number
  limit?: number | false
  file?: string
}

export const usePagesPlugin = (options?: UsePagesPluginOptions): PluginObject => {
  const name = 'vuepress-plugin-use-pages'
  const multiple = true

  const onPrepared: PluginObject['onPrepared'] = (app) => {
    const defaultSort = (a: Page, b: Page) => {
      if (!a.data.frontmatter.date || !b.data.frontmatter.date) {
        return 0
      }
      return (new Date(b.data.frontmatter.date).getTime()) - (new Date(a.data.frontmatter.date).getTime())
    }
    const {
      startsWith = '/articles/',
      filter,
      sort = defaultSort,
      limit = false,
      file = 'pages.js',
    }: UsePagesPluginOptions = options || {}

    const docs = app.pages.filter(p => p.data.path.startsWith(startsWith))
    const filtered = filter ? docs.filter(filter) : docs
    filtered.sort(sort)   // Sorted in place
    const limited = limit ? filtered.slice(0, limit) : filtered
    const pageData = limited.map(p => p.data)
    const content = `export const usePages = () => ${JSON.stringify(pageData)}`
    app.writeTemp(file, content)
  }

  return {
    name,
    multiple,
    onPrepared,
  }
}

コンポーネントをアタッチするだけのプラグインは、たとえば次のようになります。

docs/.vuepress/plugins/example/src/node/index.ts
import type { PluginObject } from '@vuepress/core'
import { path } from '@vuepress/utils'

export const examplePlugin = (options): PluginObject => {
  return {
    name: 'vuepress-plugin-example',
    clientConfigFile: path.resolve(__dirname, `../client/clientConfig.mjs`),
  }
}

クライアント側で必要なファイルを用意します。

docs/.vuepress/plugins/example/src/client/clientConfig.ts
import { defineClientConfig } from '@vuepress/client'
import MyNiceComponent from './components/MyNiceComponent.vue'

export default defineClientConfig({
  enhance({ app }) {
    app.component('MyNiceComponent', MyNiceComponent)
  },
})

beta.44 以降 clientAppEnhanceFilesclientAppRootComponentFilesclientAppSetupFiles hooks は clientConfigFile変更されました

ビルドステップとランタイム

VuePress は、静的サイトジェネレーターによるCMSです。
Vite によるビルド時に html を書き出し、実行時(ランタイム)はSPAとして振る舞います。

プラグインは、ビルドステップで実行されるものと、ランタイムで実行されるものがあり、後者は Vue.js の機能を利用するものと考えればわかりやすいでしょう。

VitePress

VuePress 2 と同様 Vite + Vue.js 3 で構築された VitePress と比較されることがあります。

VitePress は VuePress よりもミニマルな CMS です。 Vue.js の公式ドキュメントも VitePress で構築されています。

プラグインの考え方もシンプルで、ビルドを伴う拡張は Vite 自体のプラグインとして、ランタイムなどそれ以外の拡張は Vue.js のプラグインとして拡張するようです。

両者の特徴を踏まえて、選択していきたいですね。

Nuxt 3 に対応した Nuxt Content v2 も登場しました。

それではよき Vue.js ライフを!

おまけ: NPM に更新版を公開する

プラグインのディレクトリ内は、別のリポジトリにしています。
機能の修正ができ、ビルドで書き出した JavaScript による動作が確認できたら、リモートリポジトリにあげておき、その後 np により公開します。

今回も npx np --no-tests を使い package.json 内のバージョン変更および GitHub へのプッシュと NPM への更新のすべてをいっきに行ってもらいました。便利。

最後に GitHub のリリースノートの作成ページが立ち上がるので、リリースノートを更新し完了です。

Discussion