Skip to content

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

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
│   │   └── 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/index.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>;
}

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: {
    index: 'src/index.ts',
  },
  outDir: '.output',
  format: ['cjs', 'esm'],
  dts: {
    resolve: true,
  },
  splitting: false,
  sourcemap: true,
  clean: true,
  external: ['vue'], // External dependencies
  treeshake: true,
});

Vite Configuration

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts';
import { resolve } from 'node:path';

export default defineConfig({
  plugins: [
    vue(),
    dts({
      outDir: '.output',
      include: ['src/**/*'],
      exclude: ['**/*.test.*', '**/*.spec.*'],
      insertTypesEntry: true,
      copyDtsFiles: false,
      rollupTypes: true,
    }),
  ],
  build: {
    outDir: '.output',
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
      },
      name: 'ComponentLib',
      fileName: (format: string, entryName: string) => `${entryName}.${format}.js`,
    },
    rollupOptions: {
      external: ['vue', 'vue-router', 'pinia'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
});

TypeScript Configuration

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "outDir": ".output",
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.*"]
}

Available Scripts

json
{
  "scripts": {
    "dev": "vite build --watch",
    "build": "vite build",
    "type-check": "vue-tsc --noEmit",
    "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}\""
  }
}

Build and Publishing

Build for Production

bash
cd apps/my-components

# Build the component library
pnpm build

# The built files will be in the .output 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": "./.output/index.js",
  "module": "./.output/index.mjs",
  "types": "./.output/index.d.ts",
  "exports": {
    ".": {
      "types": "./.output/index.d.ts",
      "import": "./.output/index.mjs",
      "require": "./.output/index.js"
    }
  },
  "files": ["./.output"],
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "vite": "^5.0.0",
    "vue": "^3.0.0",
    "@vitejs/plugin-vue": "^5.0.0",
    "vite-plugin-dts": "^3.0.0"
  }
}

Best Practices

1. Component Design Principles

  • Single Responsibility: Each component should have one clear purpose
  • Props Interface: Define clear TypeScript interfaces for all props
  • Event Naming: Use consistent event naming conventions
  • Slot Design: Provide flexible slot APIs for customization

2. TypeScript Best Practices

  • Use strict type checking
  • Define interfaces for all public APIs
  • Export type definitions with components
  • Use generic types for reusable utilities

3. Build Optimization

  • Mark peer dependencies as external
  • Generate source maps for debugging
  • Enable tree shaking for smaller bundles
  • Optimize CSS output

4. Documentation

  • Document all public components
  • Provide usage examples
  • Include TypeScript type information
  • Keep changelog updated

5. Testing Strategy

  • Unit tests for component logic
  • Integration tests for component interaction
  • Visual regression tests for UI consistency
  • Type tests for TypeScript definitions