Component Library Template
The Component Library template provides a modern Vue 3 component library development environment with TypeScript, perfect for building reusable UI components and utility libraries.
Technical Stack
- Vue 3 - Progressive JavaScript framework
- TypeScript - Type-safe development
- tsup - TypeScript bundler
- Vite - Fast build tool and dev server
- ESBuild - Fast JavaScript bundler
- Tailwind CSS - Utility-first CSS framework
- SCSS - CSS preprocessor
Quick Start
1. Create Project
bash
# Initialize project
vup init my-component-library
cd my-component-library
# Add Component Library template
vup add my-components
2. Install Dependencies
bash
# Install dependencies
pnpm install
3. Start Development
bash
# Start development server
cd apps/my-components
pnpm dev
The component library will be available at http://localhost:9301
.
Project Structure
apps/my-components/
├── src/
│ ├── components/ # Vue components
│ │ ├── Input/ # Input component
│ │ │ ├── Input.vue # Component file
│ │ │ ├── Input.ts # Component logic
│ │ │ └── index.ts # Component export
│ │ └── index.ts # Components index
│ ├── libs/ # Utility libraries
│ │ ├── http/ # HTTP utilities
│ │ │ ├── index.ts # HTTP client
│ │ │ └── types.ts # HTTP types
│ │ └── index.ts # Libraries index
│ ├── index.ts # Main entry point
│ └── vue-shim.d.ts # Vue type declarations
├── scripts/ # Build scripts
│ └── build-index.cjs # Index generation script
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── tsup.config.ts # tsup configuration
└── vite.config.ts # Vite configuration
Core Features
Component Development
Basic Component
vue
<!-- src/components/Input/Input.vue -->
<template>
<div class="input-wrapper">
<label v-if="label" class="input-label">{{ label }}</label>
<input
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:class="inputClasses"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
/>
<div v-if="error" class="input-error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
modelValue: string;
label?: string;
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
disabled?: boolean;
error?: string;
size?: 'small' | 'medium' | 'large';
}
const props = withDefaults(defineProps<Props>(), {
type: 'text',
size: 'medium',
disabled: false,
});
const emit = defineEmits<{
'update:modelValue': [value: string];
focus: [event: FocusEvent];
blur: [event: FocusEvent];
}>();
const inputClasses = computed(() => [
'input',
`input--${props.size}`,
{
'input--error': props.error,
'input--disabled': props.disabled,
},
]);
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
const handleFocus = (event: FocusEvent) => {
emit('focus', event);
};
const handleBlur = (event: FocusEvent) => {
emit('blur', event);
};
</script>
<style lang="scss" scoped>
.input-wrapper {
@apply w-full;
}
.input-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.input {
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
@apply disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed;
&--small {
@apply px-2 py-1 text-sm;
}
&--medium {
@apply px-3 py-2;
}
&--large {
@apply px-4 py-3 text-lg;
}
&--error {
@apply border-red-300 focus:ring-red-500 focus:border-red-500;
}
&--disabled {
@apply bg-gray-50 text-gray-500 cursor-not-allowed;
}
}
.input-error {
@apply mt-1 text-sm text-red-600;
}
</style>
Component Logic
typescript
// src/components/Input/Input.ts
import { ref, computed } from 'vue';
export interface InputProps {
modelValue: string;
label?: string;
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
disabled?: boolean;
error?: string;
size?: 'small' | 'medium' | 'large';
}
export interface InputEmits {
'update:modelValue': [value: string];
focus: [event: FocusEvent];
blur: [event: FocusEvent];
}
export function useInput(props: InputProps, emit: InputEmits) {
const isFocused = ref(false);
const inputClasses = computed(() => [
'input',
`input--${props.size}`,
{
'input--error': props.error,
'input--disabled': props.disabled,
'input--focused': isFocused.value,
},
]);
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
const handleFocus = (event: FocusEvent) => {
isFocused.value = true;
emit('focus', event);
};
const handleBlur = (event: FocusEvent) => {
isFocused.value = false;
emit('blur', event);
};
return {
isFocused,
inputClasses,
handleInput,
handleFocus,
handleBlur,
};
}
Component Export
typescript
// src/components/Input/index.ts
export { default as Input } from './Input.vue';
export type { InputProps, InputEmits } from './Input';
export { useInput } from './Input';
Utility Library Development
HTTP Client
typescript
// src/libs/http/types.ts
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: any;
timeout?: number;
}
export interface HttpClientConfig {
baseURL: string;
timeout: number;
headers: Record<string, string>;
}
typescript
// src/libs/http/index.ts
import type { ApiResponse, RequestOptions, HttpClientConfig } from './types';
export class HttpClient {
private config: HttpClientConfig;
constructor(config: Partial<HttpClientConfig> = {}) {
this.config = {
baseURL: '',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
...config,
};
}
async request<T = any>(
url: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
options.timeout || this.config.timeout
);
try {
const response = await fetch(`${this.config.baseURL}${url}`, {
method: options.method || 'GET',
headers: {
...this.config.headers,
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
get<T = any>(url: string, options: Omit<RequestOptions, 'method'> = {}) {
return this.request<T>(url, { ...options, method: 'GET' });
}
post<T = any>(
url: string,
body?: any,
options: Omit<RequestOptions, 'method' | 'body'> = {}
) {
return this.request<T>(url, { ...options, method: 'POST', body });
}
put<T = any>(
url: string,
body?: any,
options: Omit<RequestOptions, 'method' | 'body'> = {}
) {
return this.request<T>(url, { ...options, method: 'PUT', body });
}
delete<T = any>(url: string, options: Omit<RequestOptions, 'method'> = {}) {
return this.request<T>(url, { ...options, method: 'DELETE' });
}
}
export const createHttpClient = (config: Partial<HttpClientConfig> = {}) => {
return new HttpClient(config);
};
export const httpClient = createHttpClient();
Development Tools
tsup Configuration
typescript
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
external: ['vue'],
treeshake: true,
minify: true,
outDir: 'dist',
outExtension({ format }) {
return {
js: `.${format}.js`,
};
},
});
Vite Configuration
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@shared': resolve(__dirname, '../../_shared'),
},
},
css: {
postcss: resolve(__dirname, '../../postcss.config.js'),
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyComponents',
fileName: (format) => `my-components.${format}.js`,
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
server: {
host: '0.0.0.0',
port: 9301,
open: false,
},
});
TypeScript Configuration
json
// tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../../_shared/*"]
}
},
"include": [
"src/**/*",
"src/**/*.vue",
"src/*.vue",
"./auto-imports.d.ts",
"src/vue-shim.d.ts"
]
}
Available Scripts
json
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && tsup",
"build:vite": "vite build",
"preview": "vite preview",
"lint": "eslint src/ --ext .vue,.ts,.js",
"lint:fix": "eslint src/ --ext .vue,.ts,.js --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss}\""
}
}
Best Practices
Component Design
vue
<template>
<div class="component" :class="componentClasses">
<slot name="header">
<h3 v-if="title" class="component-title">{{ title }}</h3>
</slot>
<div class="component-content">
<slot></slot>
</div>
<slot name="footer">
<div v-if="$slots.footer" class="component-footer">
<slot name="footer"></slot>
</div>
</slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
title?: string;
variant?: 'default' | 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'default',
size: 'medium',
disabled: false,
});
const componentClasses = computed(() => [
'component',
`component--${props.variant}`,
`component--${props.size}`,
{
'component--disabled': props.disabled,
},
]);
</script>
<style lang="scss" scoped>
.component {
@apply border rounded-lg p-4;
&--primary {
@apply border-blue-500 bg-blue-50;
}
&--secondary {
@apply border-gray-300 bg-gray-50;
}
&--small {
@apply p-2 text-sm;
}
&--medium {
@apply p-4;
}
&--large {
@apply p-6 text-lg;
}
&--disabled {
@apply opacity-50 cursor-not-allowed;
}
}
.component-title {
@apply text-lg font-semibold mb-2;
}
.component-content {
@apply mb-4;
}
.component-footer {
@apply pt-2 border-t border-gray-200;
}
</style>
Type Safety
typescript
// src/types/index.ts
export interface ComponentProps {
id?: string;
class?: string;
style?: string | Record<string, any>;
}
export interface SizeProps {
size?: 'small' | 'medium' | 'large';
}
export interface VariantProps {
variant?: 'default' | 'primary' | 'secondary' | 'danger';
}
export interface DisabledProps {
disabled?: boolean;
}
export type CommonProps = ComponentProps &
SizeProps &
VariantProps &
DisabledProps;
Documentation
typescript
// src/components/Button/Button.vue
<template>
<button
:type="type"
:disabled="disabled"
:class="buttonClasses"
@click="handleClick"
>
<slot name="icon" v-if="$slots.icon"></slot>
<span v-if="$slots.default" class="button-content">
<slot></slot>
</span>
</button>
</template>
<script setup lang="ts">
/**
* Button component
*
* @example
* <Button variant="primary" size="large" @click="handleClick">
* Click me
* </Button>
*/
import { computed } from 'vue';
interface Props {
type?: 'button' | 'submit' | 'reset';
variant?: 'default' | 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: 'button',
variant: 'default',
size: 'medium',
disabled: false,
});
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
const buttonClasses = computed(() => [
'button',
`button--${props.variant}`,
`button--${props.size}`,
{
'button--disabled': props.disabled,
},
]);
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event);
}
};
</script>
Build and Publishing
Build for Production
bash
# Build the component library
pnpm build
# The built files will be in the dist directory
Publish to NPM
bash
# Login to NPM
npm login
# Publish the package
npm publish
Package Configuration
json
// package.json
{
"name": "@your-org/my-components",
"version": "1.0.0",
"description": "A modern Vue 3 component library",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"vue": "^3.0.0"
}
}