Enhancing Integration with Unplugin and Nuxt Module

- Lire en français
Resources: huchet-vue

Having a robust component library is a great start, but integrating it into the Vue.js ecosystem takes it to another level. In this ecosystem, two main tools stand out:

  1. Unplugin Vue Components: A bundler plugin for on-demand components that are auto-imported, fully typed, and tree-shakable.
  2. Nuxt: An open-source framework based on Vue.js that simplifies and empowers web development.

Although our component library can function without integration into these tools, I'm firmly convinced that integration will enhance its power and ease of use. I'm devoted to providing the best developer experience possible, and I'm sure you will find it invaluable.

Unplugin Vue Component Resolver

Before building the resolver, it's essential to understand how Unplugin Vue Components works. This will clarify what a resolver does and its functionality.

Tip

If you have any questions, please comment below, and I'll be happy to assist.

Consider this component:

vue
<script lang="ts" setup>
</script>

<template>
  <UButton label="Click me" />
</template>

Notice that there's no import statement for the UButton component. Unplugin Vue Components scans the template and identifies non-standard HTML tags to find them in its component list. This list can be manually fed or automatically populated using a resolver.

A resolver is a function that receives a component name and returns its import path if it matches the resolver's internal list. This allows selective auto-import of components or adding prefixes to component names.

In our example, a resolver might resolve the UButton component as:

ts
return {
  name: 'Button',
  from: '@nuxt/ui'
}

Unplugin Vue Components would transform the component by injecting the import statement:

vue
<script lang="ts" setup>
import { Button as UButton } from '@nuxt/ui'
</script>

<template>
  <UButton label="Click me" />
</template>

Simple, right? Now, let's build our resolver.

Note

To deepen your understanding of Vite, try the vite-plugin-inspector, which lets you inspect modifications made by each plugin.

Building the Resolver

First, gather all your library components in a src/components.ts file within the packages/huchet-vue folder:

bash
touch src/components.ts

Then export an array of all components:

ts
export const components = [
  'Button'
]

To ensure updates, create a test that verifies the exported components match those in the components folder. Create a src/index.test.ts file in packages/huchet-vue:

bash
touch src/index.test.ts

Add this test to verify component exports:

ts
import { expect, it } from 'vitest'
import { components } from './components'
import * as Huchet from './index'

it('should expose the correct components', () => {
  expect(Object.keys(Huchet)).toEqual(Object.values(components))
})

This test prevents missed updates when components change.

Note

Inspired by Radix Vue.

Next, create a src/resolver.ts for the Unplugin Vue Components resolver and install it as a dev dependency:

bash
touch src/resolver.ts && pnpm add -D unplugin-vue-components

The resolver function initiates in vite.config.ts, allowing developers to pass options like a prefix for component names. It resolves component names and returns import paths if they match:

ts
import type { ComponentResolver } from 'unplugin-vue-components'
import { components } from './components'

export interface ResolverOptions {
  /**
   * prefix for components used in templates
   *
   * @defaultValue ""
   */
  prefix?: string
}

export default function (options: ResolverOptions = {}): ComponentResolver {
  const { prefix = '' } = options

  return {
    type: 'component',
    resolve: (name: string) => {
      if (name.toLowerCase().startsWith(prefix.toLowerCase())) {
        const componentName = name.substring(prefix.length)
        if (components.includes(componentName)) {
          return {
            name: componentName,
            from: '@barbapapazes/huchet-vue',
          }
        }
      }
    },
  }
}

To ensure the resolver is built, add it as an entry point in vite.config.ts:

ts
import { resolve } from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
        resolver: resolve(__dirname, 'src/resolver.ts'), 
      },
    },
  },
})

Finally, export the resolver in package.json:

json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs"
    },
    "./resolver": { 
      "types": "./dist/resolver.d.ts", 
      "import": "./dist/resolver.mjs"
    }
  },
  "main": "./dist/index.mjs",
  "types": "dist/index.d.ts"
}

Note

Rebuild the project to ensure the new entry point is accessible.

Using the Resolver

With the resolver ready, let's test it in our playground project:

  1. Navigate to the playground folder:
bash
cd ../../playground/vue
  1. Install unplugin-vue-components:
bash
pnpm add -D unplugin-vue-components
  1. Update vite.config.ts to use the resolver with a U prefix:
ts
import HuchetVueResolver from '@barbapapazes/huchet-vue/resolver'
import Vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    Vue(),
    Components({
      dts: true,
      resolvers: [
        HuchetVueResolver({
          prefix: 'U'
        }),
      ],
    })
  ],
})
  1. Add components.d.ts to .gitignore:
bash
echo "components.d.ts" >> .gitignore

Note

This file is generated by unplugin-vue-components to help your IDE understand the components and provide auto-completion.

  1. Update src/App.vue to use the UButton component without importing it:
vue
<template>
  <div class="flex gap-6 p-6">
    <UButton label="Hello World" />
    <UButton label="Hello World" variant="outline" />
  </div>
</template>

That's it! The UButton component is automatically imported for ease of use.

Nuxt Module

Nuxt transforms web development with Vue.js by providing an extended configuration and module system. Building a Nuxt module for our component library requires minimal effort and eliminates setup for developers. Let's dive into it.

Building the Module

Start by creating a nuxt.ts file in the packages/huchet-vue folder:

bash
touch nuxt.ts

Install the necessary Nuxt packages and Tailwind CSS:

bash
pnpm add -D @nuxt/kit @nuxt/schema && pnpm add tailwindcss@next @tailwindcss/vite@next @tailwindcss/postcss@next

Then, create the module:

ts
import type { } from '@nuxt/schema' // Mandatory to avoid a bug when building
import { addComponent, addVitePlugin, defineNuxtModule } from '@nuxt/kit'

import { components } from './components'

export interface ModuleOptions {
  prefix: string
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: '@barbapapazes/huchet-vue',
    configKey: 'huchet',
    compatibility: {
      nuxt: '>=3.0.0',
    },
  },
  defaults: {
    prefix: '',
  },
  async setup(options, nuxt) {
    if (nuxt.options.builder === '@nuxt/vite-builder') {
      const Tailwind = await import('@tailwindcss/vite').then(r => r.default)
      addVitePlugin(Tailwind())
    }
    else {
      nuxt.options.postcss.plugins['@tailwindcss/postcss'] = {}
    }

    for (const component of components) {
      addComponent({
        name: `${options.prefix}${component}`,
        export: component,
        filePath: '@barbapapazes/huchet-vue',
      })
    }
  },
})

This plugin simplifies Tailwind CSS setup, using either Vite or PostCSS depending on the Nuxt builder.

The second part auto-imports components into the Nuxt system using the Unimport framework, allowing prefix customization like the Unplugin Vue Components resolver.

Inspired by Radix Vue Nuxt module and Nuxt UI 3 module.

Add an entry in vite.config.ts and export it in package.json:

ts
import { resolve } from 'node:path'

export default defineConfig({
  build: {
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
        resolver: resolve(__dirname, 'src/resolver.ts'),
        nuxt: resolve(__dirname, 'src/nuxt.ts'), 
      },
    },
    rollupOptions: {
      external: ['vue', '@nuxt/kit', '@tailwindcss/vite'], 
    },
  },
})

Make sure to externalize @nuxt/kit and @tailwindcss/vite.

json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs"
    },
    "./resolver": {
      "types": "./dist/resolver.d.ts",
      "import": "./dist/resolver.mjs"
    },
    "./nuxt": {
      "types": "./dist/nuxt.d.ts",
      "import": "./dist/nuxt.mjs"
    }
  },
  "main": "./dist/index.mjs",
  "types": "dist/index.d.ts"
}

Using the Module

With the Nuxt module in place, let's set up a way to test its functionality. We'll create a new Nuxt playground, install the package, and configure it.

  1. Create a new Nuxt project from the root directory:
bash
npx nuxi init ./playground/nuxt

Note

Modify the postinstall script to dev:prepare in Nuxt's package.json to avoid errors after a package installation. Rename it since prepare is reserved by npm.

  1. Install the component library:
bash
cd ./playground/nuxt && pnpm add ../../packages/huchet-vue
  1. Duplicate the App.vue from the Vue playground to the Nuxt playground:
bash
cp ../vue/src/App.vue ./app.vue
  1. Add the module in nuxt.config.ts:
ts
export default defineNuxtConfig({
  modules: ['@barbapapazes/huchet-vue/nuxt'],
  css: ['~/assets/css/main.css'],
  huchet: {
    prefix: 'U',
  },
  compatibilityDate: '2024-11-01',
  devtools: { enabled: true },
})

Note

Include the CSS file using the css property for Tailwind CSS to function.

  1. Create the CSS file in assets/css/main.css:
bash
mkdir -p assets/css && touch assets/css/main.css && echo "@import 'tailwindcss';" >> assets/css/main.css
  1. Launch the Nuxt project:
bash
pnpm dev

Voila! You can now use the component library in a Nuxt project seamlessly, with auto-imported components and fully-functional Tailwind CSS.

The Nuxt developer experience, enriched by the module system, significantly simplifies developers' lives. I'm confident you will appreciate it too.

Profil Picture of Estéban

Thanks for reading! My name is Estéban, and I love to write about web development.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!