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.
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 scriptsComponent 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-utilsVitest 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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.