From Local Library to Published Package: Building Vue Notification Center
Published on 8 April 2025
Today, I want to share my journey of transforming a local Vue.js notification library into a fully-fledged npm package. This post will walk you through the process of creating, packaging, and publishing a Vue.js component library, specifically focusing on my experience with Vue Notification Center.
The Journey Begins
Like many developers, I started with a local implementation of a notification system in my Vue.js projects. It began as a simple component in my project’s lib/plugins
directory:
// The original local implementation
import { plugin } from '~/lib/plugins/NotificationCenter'
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.use(plugin))
While this worked well for individual projects, I realised that I was essentially rebuilding the same functionality across different projects. This led me to the decision to create a reusable package that could benefit the wider Vue.js community.
Creating a Standalone Package
1. Project Setup
The first step was setting up a new project with the right tools:
- Package Manager: Chose PNPM for its efficiency and disk space savings
- Build Tool: Selected Rollup for its excellent tree-shaking and module bundling capabilities
- TypeScript: Added for better type safety and developer experience
- Vue 3: Built with the latest Vue.js version for modern applications
2. Project Structure
I organised the codebase with a clear structure:
vue-notification-center/
├── src/
│ ├── components/
│ │ ├── AlertCenter.vue
│ │ ├── Notification.vue
│ │ ├── NotificationCenter.vue
│ │ ├── NotificationTypes.vue
│ │ └── Spinner.vue
│ ├── directives/
│ ├── helpers/
│ ├── lib/
│ └── scss/
├── dist/
├── package.json
└── README.md
3. Features Implementation
The package includes several key features:
- 🎯 Multiple notification types
- 📍 Customisable positions
- ⏱️ Configurable timing
- 🎨 Custom styling support
- 🔄 Reactive state management
- 🎮 Event-driven architecture
Publishing Process
1. Version Management
I implemented a clear versioning strategy following Semantic Versioning:
- Major (1.x.x): Breaking changes
- Minor (x.1.x): New features (backward compatible)
- Patch (x.x.1): Bug fixes and documentation updates
2. Build Configuration
The Rollup configuration was set up to create both UMD and ES Module builds:
export default {
input: 'src/index.js',
output: [
{
format: 'umd',
file: 'dist/vue-notification-center.umd.js'
},
{
format: 'es',
file: 'dist/vue-notification-center.es.js'
}
]
}
3. Publishing Workflow
I established a streamlined publishing process:
- Update documentation and make changes
- Commit changes with conventional commit messages
- Update version using
pnpm version
- Build the package
- Publish to npm
- Create and push Git tags
Framework Integration
Vue 3 Integration
import { createApp } from 'vue'
import App from './App.vue'
import NotificationCenter from '@harianto/vue-notification-center'
const app = createApp(App)
app.use(NotificationCenter)
app.mount('#app')
Nuxt 3 Integration
// plugins/notification-center.client.ts
import { defineNuxtPlugin } from '#app'
import { plugin } from '@harianto/vue-notification-center'
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.use(plugin))
Implementing CI/CD Pipelines
After publishing my package, I realised that manual publishing could lead to inconsistencies and errors. This led me to explore Continuous Integration and Continuous Deployment (CI/CD) pipelines to automate the build, test, and release process.
Why CI/CD Matters for Package Development
CI/CD pipelines offer several advantages for package development:
- Consistency: Ensures the same build process every time
- Automation: Reduces manual steps and human error
- Testing: Automatically runs tests before publishing
- Versioning: Handles version bumps and Git tags automatically
- Documentation: Can generate and update documentation
Setting Up GitHub Actions
I implemented a GitHub Actions workflow for my package. Here’s how it works:
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Run tests
run: pnpm test
continue-on-error: true # Continue even if tests fail for now
publish:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Install PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Publish to npm
run: pnpm publish --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Git tag
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
VERSION=$(node -p "require('./package.json').version")
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
Challenges and Solutions
During the implementation of CI/CD, I faced several challenges:
NPM Authentication:
- Challenge: How to securely authenticate with npm from GitHub Actions
- Solution: Used GitHub Secrets to store the NPM_TOKEN securely
Version Conflicts:
- Challenge: Getting "403 Forbidden" errors when trying to publish existing versions
- Solution: Implemented a version bump before publishing or manually updated versions
Test Environment:
- Challenge: Testing Vue components with custom directives that rely on browser APIs
- Solution: Created mock implementations of directives for testing
Workflow Triggers:
- Challenge: Ensuring the publish job only runs on the main branch
- Solution: Added conditional logic to the workflow:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Setting Up Testing Infrastructure
To support the CI/CD pipeline, I added a testing infrastructure:
Installed Testing Tools:
bashpnpm add -D vitest @vue/test-utils jsdom
Created Vitest Configuration:
javascript// vitest.config.js import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'], }, })
Added Test Scripts to package.json:
json"scripts": { "test": "vitest run", "test:watch": "vitest" }
Created Component Tests:
javascript// src/components/Notification.spec.js import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import Notification from './Notification.vue' // Mock the v-inject-elements directive const mockDirective = { mounted: () => {} // Empty implementation for testing } describe('Notification Component', () => { it('renders properly', () => { const wrapper = mount(Notification, { props: { notification: { title: 'Test Title', message: 'Test Message', type: 'info', elements: [], // Empty array to avoid forEach error options: { showCloseButton: true, canClose: true } } }, global: { directives: { 'inject-elements': mockDirective } } }) expect(wrapper.text()).toContain('Test Title') expect(wrapper.text()).toContain('Test Message') }) })
Benefits of CI/CD for Package Development
Implementing CI/CD has transformed my package development workflow:
- Faster Releases: Automated processes reduce the time between changes and releases
- Higher Quality: Automated testing catches issues before they reach users
- Consistent Process: Every release follows the same steps
- Better Documentation: Automated changelog generation keeps users informed
- Reduced Stress: Less manual work means fewer mistakes
Next Steps for CI/CD
I’m planning to enhance my CI/CD pipeline with:
- Automated dependency updates with Dependabot
- Code quality checks with ESLint and Prettier
- Automated documentation generation
- Performance benchmarking
- Cross-browser testing
Lessons Learned
Documentation is Crucial: Clear, comprehensive documentation makes your package more accessible and user-friendly.
Version Control: Following semantic versioning helps users understand the impact of updates.
Build Process: A well-configured build process ensures your package works across different module systems.
Framework Compatibility: Supporting multiple frameworks (Vue 3 and Nuxt 3) increases your package’s utility.
Community Focus: Building with the community in mind leads to better design decisions.
Automation is Key: CI/CD pipelines save time and reduce errors in the publishing process.
Testing is Essential: Even simple tests can catch issues before they reach users.
Security Best Practices: Using GitHub Secrets for sensitive tokens keeps your package secure.
Future Improvements
- Add comprehensive test coverage
- Implement more customisation options
- Create interactive documentation
- Add more notification types
- Improve animation performance
- Implement semantic-release for automated versioning
Conclusion
Converting a local library into a published package was a rewarding experience. It not only improved my own development workflow but also provided value to the Vue.js community. The process taught me about package development, versioning, and the importance of good documentation.
With the addition of CI/CD pipelines, my package now has a professional development workflow that ensures consistent, high-quality releases. This automation allows me to focus on improving the package rather than worrying about the mechanics of publishing.
You can find the package on npm as @harianto/vue-notification-center and the source code on GitHub.
Resources
- Vue.js Documentation
- Nuxt 3 Documentation
- npm Documentation
- Semantic Versioning
- GitHub Actions Documentation
- Vitest Documentation
This post is part of my series on Vue.js development and package publishing. Feel free to reach out if you have any questions or suggestions!