くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt3のモジュールを作るために、Module Author Guideをみてみた

Nuxt3のモジュールを作りたくなり、
いろいろ調べてみたときの備忘録(*´ω`*)

公式サイトのドキュメントを見ながら

テンプレートから作成

# テンプレートからmy-moduleディレクトリを作成
$ npx nuxi init -t module my-module

$ cd my-module

# 開発モードで使う資材の生成
$ npm run dev:prepare

# 開発サーバの起動
$ npm run dev

ディレクトリ構成

テンプレートから作成されるディレクトリ構成はこんな感じ

my-module/
  # moduleの本体
  - src/
    - runtime/
      - plugin.ts
    - module.ts
  # 開発時確認用のNuxtプロジェクト
  - playground/
    - app.vue
    - nuxt.config.ts
  # テスト資材(vitest)
  - test/
    - fixtures/
    - basic.test.ts
  - package.json

用意されているコマンド

### 開発系
# 開発時の資材作成
$ npm run dev:prepare
# 開発モードでの起動
$ npm run dev
# 開発モードでのビルド
$ npm run dev:build

### lint / test
$ npm run lint
$ npm run test
$ npm run test:watch

### 本番関係
# 本番時のビルド
$ npm run prepack
# 本番資材のリリース/公開
$ npm run release

それぞれの中身はこんな感じ。

"scripts": {
  "prepack": "nuxt-module-build",
  "dev": "nuxi dev playground",
  "dev:build": "nuxi build playground",
  "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
  "release": "pnpm run lint && pnpm run test && npm run prepack && changelogen --release && pnpm publish && git push --follow-tags",
  "lint": "eslint .",
  "test": "vitest run",
  "test:watch": "vitest watch"
},

モジュールの開発で使うファイル

src/module.ts

モジュールのエントリーポイント。

  • モジュールに関する設定(meta)
  • optionのデフォルト値(default)
  • Nuxt hooksで行う処理(hooks)
  • モジュールが読み込まれたときの処理(setup)
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'

// Module options TypeScript interface definition
export interface ModuleOptions {}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    // Usually the npm package name of your module
    name: 'my-module',
    // The key in `nuxt.config` that holds your module options
    configKey: 'myModule'
    // Compatibility constraints
    compatibility: {
      // Semver version of supported nuxt versions
      nuxt: '^3.0.0'
    }
  },
  // Default configuration options of the Nuxt module
  defaults: {},
  // Shorthand sugar to register Nuxt hooks
  hooks: {},
  // The function holding your module logic, it can be asynchronous
  setup (options, nuxt) {
    const resolver = createResolver(import.meta.url)

    // Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
    addPlugin(resolver.resolve('./runtime/plugin'))
  }
})

テンプレートの例では、プラグイン('./runtime/plugin')を追加するモジュールの例。

src/runtime/plugins.ts

こっちは、追加されるプラグインのサンプル。
"THE・サンプル"って感じのコンソールログを出すだけのソース。

// src/runtime/plugins.ts
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((nuxtApp) => {
  console.log('Plugin injected by my-module!')
})

Runtime Directory

Nuxtのモジュールは、基本的にビルドされた資材に含まれないけど、
runtimeディレクトリ配下のものは含めることはできるらしい。

runtimeディレクトリ配下に配置するとよいものは、

  • Vueコンポーネント/Stylesheets/Image etc..
  • Composables / Nuxt Plugins etc..
  • API routes / Middlewares / NitroPrugins etc..

モジュール開発用のTool

@nuxt/module-builder

モジュールのビルドツール。

@nuxt/kit

モジュール開発時のUtility。
addPluginなどの便利関数を提供。

@nuxt/test-utils

モジュールテスト時のUtility。
Nuxt applicationのsetupや実行をなどを便利に。

Recipes

Module Author Guideに書かれてるレシピ/使い方のサンプル。

Nuxt Configの変更

// src/module.ts
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    // We create the `experimental` object if it doesn't exist yet
    nuxt.options.experimental ||= {}
    nuxt.options.experimental.componentIslands = true
  }
})

複雑な構成を利用する場合、unjs/defuの利用を推奨

RuntimeConfigにモジュールのデフォルト値をマージ

nuxt.options.runtimeConfig.publicにマージすることで、
useRuntimeConfig()から使えるようになる。

// src/module.ts
import { defineNuxtModule } from '@nuxt/kit'
import { defu } from 'defu'

export default defineNuxtModule({
  setup (options, nuxt) {
    nuxt.options.runtimeConfig.public.myModule = defu(nuxt.options.runtimeConfig.public.myModule, {
      foo: options.foo
    })
  }
})

使うときはこんな感じ。

const options = useRuntimeConfig().public.myModule

プラグインの追加(addPlugin)

import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    // Create resolver to resolve relative paths
    const { resolve } = createResolver(import.meta.url)

    addPlugin(resolve('./runtime/plugin'))
  }
})

Vueコンポーネントの追加(addComponent)

import { defineNuxtModule, addComponent } from '@nuxt/kit'

export default defineNuxtModule({
  setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)

    // From the runtime directory
    addComponent({
      name: 'MySuperComponent', // name of the component to be used in vue templates
      export: 'MySuperComponent', // (optional) if the component is a named (rather than default) export
      filePath: resolver.resolve('runtime/components/MySuperComponent.vue')
    })

    // From a library
    addComponent({
      name: 'MyAwesomeComponent', // name of the component to be used in vue templates
      export: 'MyAwesomeComponent', // (optional) if the component is a named (rather than default) export
      filePath: '@vue/awesome-components'
    })
  }
})

Composablesの追加(addImports/addImportsDir)

import { defineNuxtModule, addImports, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)

    addImports({
      name: 'useComposable', // name of the composable to be used
      as: 'useComposable', 
      from: resolver.resolve('runtime/composables/useComposable') // path of composable 
    })
    
    // or
    addImportsDir(resolver.resolve('runtime/composables'))
  }
})

CSSの追加

import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    nuxt.options.css.push(resolve('./runtime/style.css'))
  }
})

画像などのpublicAssetsの追加

import { defineNuxtModule, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    nuxt.hook('nitro:config', async (nitroConfig) => {
      nitroConfig.publicAssets ||= []
      nitroConfig.publicAssets.push({
        dir: resolve('./runtime/public'),
        maxAge: 60 * 60 * 24 * 365 // 1 year
      })
    })
  }
})

Lifecycle Hookへの処理の追加

import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  // Hook to the `app:error` hook through the `hooks` map
  hooks: {
    'app:error': (err) => {
      console.info(`This error happened: ${err}`);
    }
  },
  setup (options, nuxt) {
    // Programmatically hook to the `pages:extend` hook
    nuxt.hook('pages:extend', (pages) => {
      console.info(`Discovered ${pages.length} pages`);
    })
    nuxt.hook('close', async nuxt => {
      // Your custom code here
    })
  }
})

Templates/Virtual Files(addTemplate)

import { defineNuxtModule, addTemplate } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    // The file is added to Nuxt's internal virtual file system and can be imported from '#build/my-module-feature.mjs'
    addTemplate({
      filename: 'my-module-feature.mjs',
      getContents: () => 'export const myModuleFeature = () => "hello world !"'
    })
  }
})

使い時はちょっとわからない。。

型定義の追加(addTypeTemplate)

モジュールで型定義を拡張する必要がある場合に、
d.tsファイルの生成と追加ができる。

import { defineNuxtModule, addTemplate, addTypeTemplate } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    const template = addTypeTemplate({
      filename: 'types/my-module.d.ts',
      getContents: () => `// Generated by my-module
        interface MyModuleNitroRules {
          myModule?: { foo: 'bar' }
        }
        declare module 'nitropack' {
          interface NitroRouteRules extends MyModuleNitroRules {}
          interface NitroRouteConfig extends MyModuleNitroRules {}
        }
        export {}`
    })
    
    nuxt.hook('prepare:types', ({ references }) => {
      references.push({ path: template.dst })
    })
  }
})

Testing

@nuxt/test-utilssetup$fetchを使うと、
Nuxtアプリの起動/呼び出しが簡単にできる。

import { describe, it, expect } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '@nuxt/test-utils'

describe('ssr', async () => {
  // 2. Setup Nuxt with this fixture inside your test file
  await setup({
    rootDir: fileURLToPath(new URL('./fixtures/ssr', import.meta.url)),
  })

  it('renders the index page', async () => {
    // 3. Interact with the fixture using utilities from `@nuxt/test-utils`
    const html = await $fetch('/')

    // 4. Perform checks related to this fixture
    expect(html).toContain('<div>ssr</div>')
  })
})

// 5. Repeat
describe('csr', async () => { /* ... */ })

Best Practices

非同期のモジュール / Async Modules

  • setupアップ内の待ち時間は1秒以下を推奨
  • 時間がかかる処理はNuxt hookで処理をするべき

プレフィックスを付ける / Always Prefix Exposed Interfaces

他のモジュールとの競合を避けるため、プレフィックスを付ける。

  • OK: <FooButton> / useFooBar()
  • NG: <Button> / useBar()

TypeScriptで / Be TypeScript Friendly

TypeScriptで開発しやすいように、
TypeScriptで開発し、型を公開しよう

CommonJSを避ける / Avoid CommonJS Syntax

Nuxt 3はESMネイティブなので、CommonJS構文は使わない。

その他

  • ドキュメントを用意する / Document Module Usage
  • デモを用意する / Provide a StackBlitz Demo or Boilerplate
  • Nuxtのバージョンを固定しない / Do Not Advertize With a Specific Nuxt Version
  • スターターのデフォルトをそのまま使う / Stick With Starter Defaults

モジュールの命名規則

  • @nuxt/* ... Official module
  • @nuxtjs/* ... Community module
  • nuxt-* ... Third party and other community modules
  • @my-company/nuxt-* ... Private or personal modules

なので、nuxt-*@my-company/nuxt-*でつけるのが良さそう。

Nuxt Moduleのリストに追加する

PRを出すと、公式サイトのモジュール一覧に掲載してもらえる。


以上!! ざっととした通し読みだけど、だいぶわかった気がする(*´ω`*)
よく使う設定とかは、モジュールとしてまとめておいてもよいかもしれない。

参考にしたサイト様