Frontend Development 8 min read

Onionl-UI: Building a Vue 3 Component Library – Architecture, Build Process, and Unit Testing

This article introduces Onionl-UI, a newly created Vue 3 component library, describing its motivation, current status, technology choices such as Vite and UnoCSS, directory layout, build configuration, component implementation, and unit testing with Vitest, offering a practical walkthrough for frontend engineers.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Onionl-UI: Building a Vue 3 Component Library – Architecture, Build Process, and Unit Testing

Introduction

Hey everyone! Today I want to share a special project – Onionl-UI, a component library I’m currently developing. The name comes from the idea of peeling layers like an onion and also from my GitHub nickname “Onion‑L”.

Why develop this component library?

Many may ask why create a new library when many mature ones already exist. My goal is not to reinvent the wheel but to use the project as a learning vehicle to explore frontend engineering practices.

Project status

Onionl-UI is still in its infancy. As a junior frontend developer I acknowledge many imperfections, but I believe practice is the only way to verify truth, so I aim to grow through this project and avoid abandoning it.

Technical stack: Vue 3 + Vite

Choosing Vue 3 as the base framework was natural for me. For the build tool I selected Vite because of its speed and simplicity; Vite internally uses Rollup, so migrating to Rollup later for finer‑grained control would be straightforward.

UnoCSS: Exploring atomic CSS

For styling I adopted UnoCSS, a lightweight and flexible atomic‑CSS engine. Its on‑demand generation keeps the final stylesheet extremely small, which I find impressive compared with Tailwind.

Directory structure

onionl-ui/
├── packages/                # component source code
│   ├── components/         # basic components
│   │   ├── button/
│   │   ├── input/
│   │   └── ...
│   ├── hooks/             # reusable hooks
│   ├── utils/             # utility functions
│   └── onionl-ui/         # library entry folder
├── docs/                  # documentation site
├── play/                  # component preview playground
├── preset/                # UnoCSS presets
└── scripts/              # build scripts

Component library packaging

The packaging step determines how the library can be consumed by other developers. Below is the core build script that collects source files, generates different output formats, and copies necessary documentation.

Core packaging logic

async function buildAll() {
  // 1. Collect all source files to be bundled
  const input = excludeFiles(await glob('**/*.{js,ts,vue}', {
    cwd: pkgPath,
    absolute: true,
    onlyFiles: true,
  }))

  // 2. Build for each output format
  buildConfig.forEach(async ({ outPath, format, extend }) => {
    await build({
      build: {
        rollupOptions,
        minify: false,  // keep code readable for debugging
        sourcemap: true,
        outDir: resolve(rootPath, outPath),
        lib: {
          entry: input,
          formats: [format],
          fileName: () => `[name].${extend}`
        }
      },
      plugins: [
        vue(),
        vueJsx(),
        UnoCSS(),
        dts({
          include: ['packages/**/**/*.{vue,ts,tsx}'],
          exclude: ['packages/**/test/**'],
          outDir: 'dist/es',
          staticImport: true,
          insertTypesEntry: true,
        })
      ]
    })
  })

  // 3. Copy necessary documentation files
  await copyFiles()
}

Components are written as regular Vue single‑file components and can also use defineComponent together with TSX/JSX for higher flexibility and performance.

Unit testing: Starting with the Button component

The first component I implemented is a simple Button. I chose Vitest as the test runner because it integrates seamlessly with the Vite ecosystem.

Install the required dependencies:

pnpm i -D vitest happy-dom @vue/test-utils

Vitest needs a DOM environment, which is provided by happy-dom . The Vue testing utilities simplify component testing.

Vitest configuration:

import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    globals: true,
    testTimeout: 10000,
    coverage: {
      reporter: ['text', 'json', 'html'],
      exclude: ['play/**'],
    },
  },
})

A basic test case for the Button component:

describe('button Component', () => {
  it('renders default button correctly', () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Button Text',
      },
    })

    expect(wrapper.classes()).toContain('ol-button')
    expect(wrapper.text()).toBe('Button Text')
    expect(wrapper.classes()).toContain('ol-button__size-sm')
    expect(wrapper.classes()).toContain('ol-button__type-primary')
  })
})

Conclusion

The purpose of building this library is not to duplicate existing solutions but to deepen my understanding of frontend engineering through hands‑on practice. Although many aspects remain imperfect, that very imperfection provides room for improvement and learning.

frontendTestingVuecomponent librarybuildviteUnoCSS
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.