React Component Library: Membuat dan Publish NPM Package Sendiri

Apa itu Component Library?

Component library itu sebenarnya seperti koleksi toolkit untuk developer. Bayangin kamu punya kotak perkakas yang berisi berbagai komponen UI yang siap dipakai kapan aja.

Jadi, daripada bikin tombol, form, atau card dari nol terus-terusan, kamu tinggal ambil dari "kotak perkakas" ini. Praktis banget kan?

Dalam konteks React, component library adalah kumpulan komponen yang sudah dibuat, ditest, dan dikemas dengan rapi sehingga bisa dipake di berbagai project.

Misalnya, kamu bikin komponen Button yang sudah ada berbagai varian:

  • Primary button untuk aksi utama
  • Secondary button untuk aksi pendukung
  • Danger button untuk aksi berbahaya

Nah, komponen ini bisa kamu pake di project BuildWithAngga, project client, atau project personal tanpa harus coding ulang.

Yang bikin component library ini powerful adalah konsistensi dan reusability. Bayangin kalo kamu kerja di tim yang besar, semua orang bikin tombol dengan style yang berbeda-beda. Jadinya UI nggak konsisten dan maintenance-nya susah banget.

Dengan component library, semua developer pake komponen yang sama, jadi design system-nya terjaga.

Kenapa Perlu Bikin Sendiri?

Terus kenapa sih kita perlu bikin sendiri? Bukannya udah ada banyak library siap pake seperti Material-UI, Ant Design, atau Chakra UI?

Nah, ini pertanyaan bagus. Memang library-library populer ini udah sangat mature dan feature-complete. Tapi ada beberapa alasan kenapa kita mungkin perlu bikin sendiri:

1. Customization yang Lebih Bebas

Library populer memang powerful, tapi kadang kita butuh sesuatu yang sangat spesifik sesuai brand atau requirement khusus.

Misalnya, di BuildWithAngga kamu butuh komponen yang punya interaction pattern yang unik, atau styling yang sangat berbeda dari library mainstream.

2. Bundle Size yang Lebih Kecil

Library besar kayak Material-UI itu comprehensive banget, tapi kadang kita cuma butuh beberapa komponen aja.

Bikin library sendiri memungkinkan kita cuma include apa yang kita butuhkan, jadi aplikasi kita lebih ringan dan loading-nya lebih cepat.

3. Learning Experience yang Berharga

Ini yang paling penting, terutama buat kamu yang lagi belajar. Bikin component library sendiri itu proses pembelajaran yang luar biasa.

Kamu akan belajar tentang API design, accessibility, testing, bundling, dan banyak hal lain yang bakal bikin kamu jadi developer yang lebih baik.

Contoh Library Populer

Material-UI adalah salah satu library paling populer di ecosystem React. Mereka punya komponen-komponen yang mengikuti Material Design guideline dari Google.

Ada juga Ant Design yang populer banget di komunitas enterprise, atau Bootstrap React yang mengadaptasi framework CSS Bootstrap ke dalam bentuk React components.

Yang menarik dari library-library ini adalah mereka nggak cuma provide komponen, tapi juga design system yang komprehensif. Ada theming, spacing, color palette, typography, dan berbagai aspek design lainnya yang udah dipikirkan matang-matang.

Ini yang akan kita coba replicate dalam versi kita sendiri nanti.

Persiapan Awal

Sebelum kita mulai bikin component library yang keren, ada beberapa hal yang perlu dipersiapkan dulu. Jangan khawatir, prosesnya nggak ribet kok. Anggap aja kayak nyiapin bahan sebelum masak.

Yang Perlu Diinstal di Komputer

Pertama-tama, pastiin komputer kamu udah ada Node.js versi terbaru. Kenapa? Karena kita butuh npm untuk manage dependencies dan publish package nanti.

Kalo belum ada, download aja di nodejs.org. Pilih yang LTS version biar lebih stabil. Setelah install, coba cek di terminal:

node --version
npm --version

Terminal: versi node dan npm
Terminal: versi node dan npm

Kalo udah muncul nomor versinya, berarti instalasi berhasil. Selain itu, pastiin juga punya code editor favorit kayak VS Code. Trust me, extension-extension di VS Code bakal sangat membantu development process nanti.

Oh ya, install juga Git kalo belum ada. Kita bakal butuh version control untuk track perubahan code. Plus, nanti kita juga akan integrate dengan GitHub untuk dokumentasi dan repository.

Membuat Akun NPM

Nah, ini step penting banget. NPM (Node Package Manager) adalah tempat kita bakal publish library kita supaya orang lain bisa pake. Kayak toko online tapi khusus untuk package JavaScript.

Tampilan NPM profile
Tampilan NPM profile

Langkah-langkahnya gampang:

  1. Buka npmjs.com
  2. Klik "Sign Up" di pojok kanan atas
  3. Isi username, email, sama password
  4. Verifikasi email kamu
  5. Login ke terminal pake npm login

Yang perlu diinget, username NPM ini bakal jadi bagian dari package name kamu. Jadi pilih yang bagus dan profesional. Misalnya kalo username kamu "buildwithangga", nanti package kamu bisa jadi "@buildwithangga/ui-components".

Setelah daftar, jangan lupa verify account melalui email. NPM agak strict soal ini, jadi pastiin account kamu fully verified sebelum lanjut.

Setup Project Baru dengan Vite

Sekarang saatnya bikin project baru. Kita bakal pake Vite sebagai build tool karena dia cepet banget dan cocok untuk modern JavaScript development.

Kenapa Vite? Karena dia punya hot module replacement yang super cepat, support TypeScript out of the box, dan bundling-nya efisien. Perfect buat component library.

Buat project baru dengan command ini:

npx create-vite@latest buildwithangga-ui --template react-ts
cd buildwithangga-ui
npm install

Terminal: Buat project
Terminal: Buat project

Command di atas bakal bikin project React dengan TypeScript template. Kenapa TypeScript? Karena type safety itu penting banget untuk library yang bakal dipake banyak orang.

Setelah instalasi selesai, coba jalanin development server:

npm run dev

Tampilan awal vite + react
Tampilan awal vite + react

Kalo berhasil, browser bakal buka halaman dengan Vite + React logo. Ini tandanya setup awal udah berhasil dan kita siap lanjut ke tahap berikutnya.

Struktur folder project kamu sekarang kira-kira kayak gini:

buildwithangga-ui/
├── src/
├── public/
├── package.json
├── vite.config.ts
└── tsconfig.json

Di tahap ini, project kita masih berupa React app biasa. Nanti kita akan modifikasi struktur dan konfigurasi supaya cocok untuk component library.

Membuat Component Pertama

Sekarang kita lanjutin dari Button component yang udah kita buat sebelumnya. Di pembahasan sebelumnya kita udah bikin struktur dasar Button dengan TypeScript types-nya. Sekarang saatnya kita enhance component tersebut dengan styling yang lebih proper dan functionality yang lebih lengkap.

Membuat Button Component Sederhana

Button component yang udah kita buat di pembahasan sebelumnya masih pake inline styles dengan Tailwind classes. Sekarang kita bakal upgrade ke CSS Modules supaya lebih maintainable dan professional.

Pertama, kita modifikasi struktur folder Button supaya include CSS file:

src/components/Button/
├── Button.tsx (sudah ada)
├── Button.types.ts (sudah ada)
├── Button.module.css (baru)
└── index.ts (sudah ada)

Kita bakal ganti implementasi Button.tsx yang sebelumnya pake Tailwind classes jadi pake CSS Modules. Ini lebih scalable dan mudah di-customize.

Cara Menulis Props dengan TypeScript

Button.types.ts yang udah kita buat sebelumnya masih basic banget. Mari kita enhance dengan props yang lebih comprehensive untuk real-world usage:

export interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  type?: 'button' | 'submit' | 'reset'
  className?: string
  fullWidth?: boolean
  icon?: React.ReactNode
  iconPosition?: 'left' | 'right'
}

Yang baru kita tambahkan:

  • danger variant untuk aksi destructive
  • loading state dengan spinner
  • fullWidth untuk button yang memenuhi container
  • icon dan iconPosition untuk button dengan icon

Sekarang update Button.tsx untuk menggunakan CSS Modules dan props yang baru:

import React from 'react'
import { ButtonProps } from './Button.types'
import styles from './Button.module.css'

const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'md',
  disabled = false,
  loading = false,
  onClick,
  type = 'button',
  className = '',
  fullWidth = false,
  icon,
  iconPosition = 'left'
}) => {
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    if (disabled || loading) return
    onClick?.(event)
  }

  const buttonClasses = [
    styles.button,
    styles[variant],
    styles[size],
    fullWidth && styles.fullWidth,
    disabled && styles.disabled,
    loading && styles.loading,
    className
  ].filter(Boolean).join(' ')

  const LoadingSpinner = () => (
    <div className={styles.spinner}>
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
        <circle
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          strokeWidth="3"
          strokeLinecap="round"
          strokeDasharray="31.416"
          strokeDashoffset="31.416"
        />
      </svg>
    </div>
  )

  const renderContent = () => {
    if (loading) {
      return (
        <>
          <LoadingSpinner />
          Loading...
        </>
      )
    }

    if (icon && iconPosition === 'left') {
      return (
        <>
          <span className={styles.icon}>{icon}</span>
          {children}
        </>
      )
    }

    if (icon && iconPosition === 'right') {
      return (
        <>
          {children}
          <span className={styles.icon}>{icon}</span>
        </>
      )
    }

    return children
  }

  return (
    <button
      type={type}
      className={buttonClasses}
      onClick={handleClick}
      disabled={disabled || loading}
    >
      {renderContent()}
    </button>
  )
}

export default Button

Styling dengan CSS Biasa

Sekarang kita buat CSS yang comprehensive di Button.module.css. Ini akan replace Tailwind classes yang sebelumnya kita pake:

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 0;
  font-weight: 600 !important;
  cursor: pointer;
  transition: all 0.4s;
  text-decoration: none;
  font-family: inherit;
  position: relative;
  overflow: hidden;
  width: fit-content !important;
  height: fit-content !important;
  line-height: 24px;
}

.button:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

/* Variant styles */
.primary {
  background: #2447f9;
  color: #fff;
  border-radius: 50px;
}

.primary:hover:not(:disabled) {
  box-shadow: 0 0 0 .25rem #0d6efd40 !important;
  background: #2447f9;
  color: #fff;
  border: 0;
  transition: all .4s;
}

.secondary {
  background-color: #f3f4f6;
  color: #374151;
  border-radius: 50px;
}

.secondary:hover:not(:disabled) {
  background-color: #e5e7eb;
  box-shadow: 0 0 0 .25rem rgba(243, 244, 246, 0.4);
}

.outline {
  background-color: transparent;
  color: #2447f9;
  border: 2px solid #2447f9;
  border-radius: 50px;
}

.outline:hover:not(:disabled) {
  background-color: #2447f9;
  color: white;
  box-shadow: 0 0 0 .25rem #0d6efd40;
}

.ghost {
  background-color: transparent;
  color: #374151;
  border-radius: 50px;
}

.ghost:hover:not(:disabled) {
  background-color: #f3f4f6;
  box-shadow: 0 0 0 .25rem rgba(243, 244, 246, 0.4);
}

.danger {
  background: #ef4444;
  color: white;
  border-radius: 50px;
}

.danger:hover:not(:disabled) {
  background: #dc2626;
  box-shadow: 0 0 0 .25rem rgba(239, 68, 68, 0.25);
}

/* Size styles */
.sm {
  padding: 8px 16px;
  font-size: 14px;
}

.md {
  padding: 12px 24px;
  font-size: 16px;
}

.lg {
  padding: 16px 32px;
  font-size: 18px;
}

/* Modifier styles */
.fullWidth {
  width: 100% !important;
}

.disabled {
  opacity: 0.5;
  cursor: not-allowed;
  box-shadow: none !important;
}

.loading {
  pointer-events: none;
}

.icon {
  display: flex;
  align-items: center;
}

.spinner {
  display: flex;
  align-items: center;
  animation: spin 1s linear infinite;
}

.spinner svg {
  animation: dash 1.5s ease-in-out infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes dash {
  0% {
    stroke-dasharray: 1, 150;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -35;
  }
  100% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -124;
  }
}

/* Responsive design */
@media screen and (max-width: 575px) {
  .primary {
    font-size: 14px;
    background: #3d58ff;
  }

  .sm {
    font-size: 12px;
    padding: 6px 12px;
  }

  .md {
    font-size: 14px;
    padding: 10px 20px;
  }

  .lg {
    font-size: 16px;
    padding: 12px 24px;
  }
}

Sekarang Button component kita udah much better! Bisa dipake untuk berbagai keperluan di website BuildWithAngga. Update file App.tsx untuk test semua variant:

import { Button } from './components'

function App() {
  const handleDelete = () => {
    alert('Akun akan dihapus!')
  }

  return (
    <div style={{ 
      padding: '40px', 
      maxWidth: '800px', 
      margin: '0 auto',
      fontFamily: 'system-ui, sans-serif'
    }}>
      <h1 style={{ marginBottom: '30px', color: '#2447f9' }}>
        BuildWithAngga UI Components
      </h1>
      
      {/* Button CTA utama */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Button CTA Utama
        </h3>
        <Button variant="primary" size="lg" fullWidth>
          Daftar Kelas Premium
        </Button>
      </div>

      {/* Button dengan icon */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Button dengan Icon
        </h3>
        <Button variant="outline" icon="📥" iconPosition="left">
          Download Materi
        </Button>
      </div>

      {/* Button loading state */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Button Loading State
        </h3>
        <Button variant="primary" loading>
          Menyimpan...
        </Button>
      </div>

      {/* Button bahaya */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Button Bahaya
        </h3>
        <Button variant="danger" onClick={handleDelete}>
          Hapus Akun
        </Button>
      </div>

      {/* Showcase semua variants */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Semua Variants
        </h3>
        <div style={{ display: 'flex', gap: '15px', flexWrap: 'wrap' }}>
          <Button variant="primary">Primary</Button>
          <Button variant="secondary">Secondary</Button>
          <Button variant="outline">Outline</Button>
          <Button variant="ghost">Ghost</Button>
          <Button variant="danger">Danger</Button>
        </div>
      </div>

      {/* Showcase semua sizes */}
      <div>
        <h3 style={{ marginBottom: '15px', color: '#374151' }}>
          Semua Sizes
        </h3>
        <div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
          <Button variant="primary" size="sm">Small</Button>
          <Button variant="primary" size="md">Medium</Button>
          <Button variant="primary" size="lg">Large</Button>
        </div>
      </div>
    </div>
  )
}

export default App
image.png
Button Component

Component Button kita sekarang udah production-ready dan siap jadi foundation untuk component library BuildWithAngga UI!

Testing Component

Testing itu kayak quality control di pabrik - penting banget untuk mastiin component kita berfungsi dengan baik sebelum dipake sama orang lain. Apalagi untuk component library, testing jadi krusial karena kalo ada bug, bisa ngerusak aplikasi yang pake library kita.

Setup Testing yang Mudah

Untuk testing React component, kita bakal pake Vitest dan React Testing Library. Vitest ini modern test runner yang dibuat sama tim Vite - jadi integrasinya seamless banget dengan project Vite kita.

Kenapa Vitest? Karena dia super cepat, punya hot module replacement untuk test, dan nggak butuh konfigurasi ribet. Plus, API-nya compatible dengan Jest jadi migrasi dari Jest gampang.

Pertama, install dependencies yang dibutuhkan:

npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @testing-library/dom

React Testing Library ini filosofinya bagus banget - dia ngetes component dari sudut pandang user, bukan implementasi detail. Jadi kita ngetest apa yang user liat dan interaksi yang user lakuin.

Update vite.config.ts untuk nambahin konfigurasi test:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
    css: true
  }
})

Konfigurasi globals: true memungkinkan kita pake global functions kayak describe, test, expect tanpa import. Environment jsdom diperluin untuk DOM simulation di test.

Buat file src/setupTests.ts:

import '@testing-library/jest-dom'

File ini akan dijalanin sebelum semua test, jadi kita bisa setup global configuration di sini. @testing-library/jest-dom memberikan custom matchers kayak toBeInTheDocument() yang sangat berguna.

Update package.json untuk nambahin script test:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}

Menulis Test Sederhana untuk Button

Sekarang kita bikin test untuk Button component. Buat file src/components/Button/Button.test.tsx:

import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, test, expect, vi } from 'vitest'
import Button from './Button'

describe('Button Component', () => {
  test('renders dengan children text', () => {
    render(<Button>Click me</Button>)

    const buttonElement = screen.getByRole('button', { name: /click me/i })
    expect(buttonElement).toBeInTheDocument()
  })

  test('menerapkan variant styling dengan benar', () => {
    render(<Button variant="primary">Primary Button</Button>)

    const buttonElement = screen.getByRole('button')
    // CSS Modules mengubah class name, jadi kita cek apakah ada class yang mengandung 'primary'
    expect(buttonElement.className).toMatch(/primary/)
  })

  test('menerapkan size styling dengan benar', () => {
    render(<Button size="lg">Large Button</Button>)

    const buttonElement = screen.getByRole('button')
    expect(buttonElement.className).toMatch(/lg/)
  })

  test('memanggil onClick handler ketika diklik', async () => {
    const handleClick = vi.fn()
    const user = userEvent.setup()

    render(<Button onClick={handleClick}>Clickable</Button>)

    const buttonElement = screen.getByRole('button')
    await user.click(buttonElement)

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  test('tidak memanggil onClick ketika disabled', async () => {
    const handleClick = vi.fn()
    const user = userEvent.setup()

    render(<Button onClick={handleClick} disabled>Disabled</Button>)

    const buttonElement = screen.getByRole('button')
    await user.click(buttonElement)

    expect(handleClick).not.toHaveBeenCalled()
    expect(buttonElement).toBeDisabled()
  })

  test('menampilkan loading state dengan benar', () => {
    render(<Button loading>Loading Button</Button>)

    const buttonElement = screen.getByRole('button')
    const loadingText = screen.getByText(/loading/i)

    expect(buttonElement).toBeDisabled()
    expect(loadingText).toBeInTheDocument()
    expect(buttonElement.className).toMatch(/loading/)
  })

  test('menampilkan icon dengan posisi yang benar', () => {
    render(
      <Button icon="🔥" iconPosition="left">
        Hot Button
      </Button>
    )

    const buttonElement = screen.getByRole('button')
    const iconElement = screen.getByText('🔥')

    expect(buttonElement).toBeInTheDocument()
    expect(iconElement).toBeInTheDocument()
  })

  test('menerapkan fullWidth styling', () => {
    render(<Button fullWidth>Full Width</Button>)

    const buttonElement = screen.getByRole('button')
    expect(buttonElement.className).toMatch(/fullWidth/)
  })

  test('menerima custom className', () => {
    render(<Button className="custom-class">Custom</Button>)

    const buttonElement = screen.getByRole('button')
    expect(buttonElement).toHaveClass('custom-class')
  })

  test('menampilkan loading spinner SVG', () => {
    render(<Button loading>Saving...</Button>)

    const svgElement = screen.getByRole('button').querySelector('svg')
    expect(svgElement).toBeInTheDocument()
    expect(svgElement).toHaveAttribute('width', '16')
    expect(svgElement).toHaveAttribute('height', '16')
  })

  test('button memiliki struktur class yang benar', () => {
    render(<Button variant="secondary" size="lg" disabled>Test</Button>)

    const buttonElement = screen.getByRole('button')

    // Cek apakah semua class CSS Modules ada
    expect(buttonElement.className).toMatch(/button/)
    expect(buttonElement.className).toMatch(/secondary/)
    expect(buttonElement.className).toMatch(/lg/)
    expect(buttonElement.className).toMatch(/disabled/)
  })
})

Test-test di atas cover semua fitur utama Button component kita. Kita ngetest:

  • Rendering basic text
  • Variant dan size styling
  • Event handling (onClick)
  • Disabled state behavior
  • Loading state functionality
  • Icon positioning
  • FullWidth modifier
  • Custom className
  • SVG spinner rendering

Yang keren dari React Testing Library adalah dia maksa kita ngetest dari perspektif user. Kita cari element pake getByRole('button') atau getByText(), bukan pake class names atau implementation details.

Cara Menjalankan Test

Sekarang jalanin test dengan command:

npm test

Terminal: Test Button
Terminal: Test Button

Vitest bakal running dalam watch mode secara default, jadi dia automatically re-run test setiap kali ada perubahan file. Sangat berguna untuk development workflow.

Kalo semua test pass, kamu bakal liat output hijau yang menunjukkan semua test berhasil. Kalo ada yang gagal, Vitest bakal kasih detail error yang sangat helpful dengan source code preview.

Untuk liat UI testing yang interaktif:

npm run test:ui

Test Button
Test Button

Command ini bakal buka browser dengan interface yang keren untuk ngeliat test results, coverage, dan debugging. UI ini sangat membantu untuk understand test behavior.

Untuk liat coverage report:

npm run test:coverage

Terminal: test coverage
Terminal: test coverage

Command ini bakal generate laporan seberapa banyak code kita yang ke-cover sama test. Target yang bagus adalah minimal 80% coverage, tapi quality test lebih penting daripada quantity.

Output coverage bakal nunjukin file mana yang belum fully tested dan baris code mana yang belum ke-cover. Ini membantu kita identify area yang butuh more testing.

Yang bikin Vitest superior dibanding Jest adalah speed dan developer experience. Test berjalan much faster dan hot reload untuk test bikin development cycle jadi sangat smooth.

Dengan testing setup ini, kita bisa confident bahwa Button component kita bekerja dengan baik dan ready untuk dipake di production. Plus, kalo ada perubahan di masa depan, test bakal langsung kasih tau kalo ada functionality yang rusak.

Build untuk Production

Nah, sekarang kita masuk ke tahap yang crucial banget - build component library kita untuk production. Ini kayak packaging produk sebelum dijual ke customer. Kita perlu pastiin library kita ter-bundle dengan benar dan optimized supaya performa aplikasi yang pake library kita tetap bagus.

Konfigurasi Build dengan Vite

Vite punya kemampuan luar biasa untuk build library. Yang bikin dia special adalah dia bisa generate multiple output format sekaligus - CommonJS untuk Node.js environment dan ES Modules untuk modern bundlers.

Pertama, kita perlu update vite.config.ts untuk library mode. Edit file tersebut jadi kayak gini:

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'BuildWithAnggaUI',
      formats: ['es', 'cjs'],
      fileName: (format) => `buildwithangga-ui.${format}.js`
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    },
    sourcemap: true,
    emptyOutDir: true
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
    css: true
  }
})

Konfigurasi ini ngasih tau Vite bahwa kita mau build library, bukan aplikasi biasa. Entry point kita adalah src/index.ts yang bakal jadi main export file. Format es dan cjs memastiin compatibility dengan berbagai environment.

Yang penting adalah external configuration. Kita mark React dan ReactDOM sebagai external dependencies, artinya mereka nggak bakal di-bundle ke library kita. Ini penting karena aplikasi yang pake library kita pasti udah ada React-nya sendiri.

Sekarang buat file src/index.ts sebagai main entry point:

// Export semua components
export { default as Button } from './components/Button'

// Export types
export type { ButtonProps } from './components/Button/Button.types'

// Styles jika diperlukan
import'./styles/globals.css'

File ini adalah gerbang utama library kita. Semua component dan types yang mau kita expose ke user harus di-export di sini. Think of it kayak daftar menu di restoran - apa aja yang bisa dipesan customer.

Buat juga file src/styles/globals.css untuk global styles yang mungkin dibutuhkan:

/* Reset dan base styles untuk BuildWithAngga UI */
.buildwithangga-ui * {
  box-sizing: border-box;
}

/* CSS Variables untuk theming */
:root {
  --bwa-primary: #2447f9;
  --bwa-secondary: #f3f4f6;
  --bwa-danger: #ef4444;
  --bwa-border-radius: 50px;
  --bwa-font-weight: 600;
  --bwa-transition: all 0.4s;
}

Cara Build Project jadi Library

Sebelum build, kita perlu setup beberapa konfigurasi penting. Pertama, buat file tsconfig khusus untuk build library.

Buat file tsconfig.lib.json:

{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist",
    "outDir": "./dist",
    "emitDeclarationOnly": true,
    "noEmit": false
  },
  "include": [
    "src/index.ts",
    "src/components/**/*",
    "src/types/**/*"
  ],
  "exclude": [
    "**/*.test.*",
    "src/App.tsx",
    "src/main.tsx",
    "src/setupTests.ts"
  ]
}

Kenapa perlu tsconfig terpisah? Karena Vite menggunakan project references dengan multiple tsconfig files. Konfigurasi ini fokus untuk generate type definitions saja.

Jangan lupa buat type declarations untuk CSS Modules. Buat file src/types/css-modules.d.ts:

declare module '*.module.css' {
  const classes: { [key: string]: string }
  export default classes
}

declare module '*.module.scss' {
  const classes: { [key: string]: string }
  export default classes
}

Sekarang update package.json dengan informasi library yang proper:

{
  "name": "@withcakfan/buildwithangga-ui",
  "version": "1.0.0",
  "description": "Modern React component library untuk BuildWithAngga projects",
  "main": "./dist/buildwithangga-ui.cjs.js",
  "module": "./dist/buildwithangga-ui.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/buildwithangga-ui.es.js",
      "require": "./dist/buildwithangga-ui.cjs.js"
    },
    "./dist/buildwithangga-ui.css": "./dist/buildwithangga-ui.css"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vite build && npm run build:types",
    "build:types": "tsc --project tsconfig.lib.json",
    "build:watch": "vite build --watch",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@vitejs/plugin-react": "^4.0.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  },
  "keywords": [
    "react",
    "components",
    "ui",
    "buildwithangga",
    "typescript",
    "vite"
  ],
  "repository": {
    "type": "git",
    "url": "<https://github.com/buildwithangga/ui-components>"
  },
  "bugs": {
    "url": "<https://github.com/buildwithangga/ui-components/issues>"
  },
  "homepage": "<https://github.com/buildwithangga/ui-components#readme>",
  "author": "BuildWithAngga Team",
  "license": "MIT"
}

Yang penting di sini adalah:

  • main untuk CommonJS entry point
  • module untuk ES Module entry point
  • types untuk TypeScript definitions
  • exports untuk modern Node.js resolution dengan CSS export yang benar
  • peerDependencies untuk React (user harus provide sendiri)
  • files array yang specify apa aja yang di-include dalam NPM package

Sekarang jalankan build command:

npm run build

Terminal: build component libary
Terminal: build component libary

Command ini bakal jalanin Vite build dulu untuk generate JavaScript bundles dan CSS, terus TypeScript compiler untuk generate type definitions. Urutan ini penting supaya file .d.ts nggak kehapus sama Vite yang by default nge-clear folder dist.

Cek Hasil Build

Setelah build selesai, cek folder dist/. Kamu harusnya liat struktur kayak gini:

dist/
├── buildwithangga-ui.css         # Compiled CSS
├── buildwithangga-ui.cjs.js      # CommonJS bundle
├── buildwithangga-ui.cjs.js.map  # Source map for CJS
├── buildwithangga-ui.es.js       # ES Module bundle
├── buildwithangga-ui.es.js.map   # Source map for ESM
├── index.d.ts                    # Main type definitions
└── components/                   # Component type definitions
    └── Button/
        ├── Button.d.ts
        ├── Button.types.d.ts
        └── index.d.ts

Buka file buildwithangga-ui.es.js dan cek isinya. Kamu bakal liat kode yang udah di-minify dan optimized. React dan ReactDOM nggak akan ada di dalam bundle karena kita mark sebagai external.

File CSS buildwithangga-ui.css berisi semua styles dari component kita yang udah di-compile dan optimized. User bisa import CSS ini terpisah kalo mereka mau.

Type definitions di index.d.ts akan memungkinkan TypeScript users dapat IntelliSense dan type checking ketika pake library kita.

Test juga build result dengan cara import di project lain:

# Di project lain
npm install file:../path/to/buildwithangga-ui

# Atau test dengan npm pack dulu
npm pack
# Ini akan generate buildwithangga-ui-components-1.0.0.tgz

File .tgz ini adalah exactly apa yang bakal di-upload ke NPM registry nanti. Kamu bisa extract dan cek isinya untuk mastiin semua file yang dibutuhin ada di sana.

Bundle size juga penting untuk di-check. File ES Module harusnya relatif kecil karena cuma contain component logic tanpa dependencies. Kalo terlalu besar, mungkin ada dependencies yang accidentally ke-bundle.

Testing Library di Project Baru

Setelah build selesai, sangat penting untuk test library kita di project terpisah. Ini memastiin library berfungsi seperti yang diharapkan user.

Pertama, kita pack library jadi file .tgz kayak yang bakal di-publish ke NPM:

# Di folder library buildwithangga-ui
npm pack

Command ini bakal generate file withcakfan-buildwithangga-ui-1.0.0.tgz yang berisi exactly apa yang akan di-upload ke NPM registry nanti.

Sekarang buat project React baru untuk testing:

# Keluar dari folder library dulu
cd ..

# Buat project test baru
npx create-vite@latest test-buildwithangga-ui --template react-ts
cd test-buildwithangga-ui
npm install

Install library kita dari file .tgz:

# Install dari file pack yang udah di-generate
npm install ../buildwithangga-ui/withcakfan-buildwithangga-ui-1.0.0.tgz

Test import dan usage di src/App.tsx:

import { Button } from '@withcakfan/buildwithangga-ui'
// Import CSS dengan path lengkap
import '@withcakfan/buildwithangga-ui/dist/buildwithangga-ui.css'

function App() {
  return (
    <div style={{ padding: '20px' }}>
      <h1>Testing BuildWithAngga UI</h1>

      <div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
        <Button variant="primary">Primary Button</Button>
        <Button variant="secondary">Secondary Button</Button>
        <Button variant="outline">Outline Button</Button>
      </div>

      <div style={{ marginTop: '20px' }}>
        <Button variant="primary" loading>
          Loading Button
        </Button>
      </div>

      <div style={{ marginTop: '20px' }}>
        <Button variant="danger" onClick={() => alert('Clicked!')}>
          Delete Action
        </Button>
      </div>
    </div>
  )
}

export default App

Jalankan test project:

npm run dev

Test component di proyek baru
Test component di proyek baru

Kalo semua button muncul dengan styling yang benar, TypeScript IntelliSense berfungsi, dan nggak ada error, berarti library kita sukses!

File .tgz ini juga bisa kamu extract untuk melihat exactly apa yang akan di-publish:

tar -tzf withcakfan-buildwithangga-ui-1.0.0.tgz

Testing dengan npm pack ini lebih reliable karena simulasi exact sama dengan publish ke NPM registry. Plus, kamu bisa distribute file .tgz ini ke teman untuk testing sebelum publish resmi.

Testing di project terpisah ini crucial karena environment development library beda dengan environment production user.

Build configuration yang baik akan menghasilkan library yang ringan, tree-shakeable, dan compatible dengan berbagai build tools modern.

Dokumentasi dengan Storybook

Dokumentasi itu kayak manual book untuk component library kita. Tanpa dokumentasi yang baik, developer lain bakal bingung gimana cara pake component kita. Di sinilah Storybook berperan penting - dia bisa bikin dokumentasi interaktif yang memungkinkan developer explore component dengan berbagai props dan state.

Install dan Setup Storybook

Storybook adalah tool yang powerful banget untuk develop dan document UI component secara isolated. Dengan Storybook, kita bisa liat component dalam berbagai kondisi tanpa harus bikin halaman khusus atau mock data yang ribet.

Install Storybook di project kita:

# Jalankan di root folder buildwithangga-ui
npx storybook@latest init

Command ini bakal detect project type kita (React dengan Vite) dan install dependencies yang dibutuhkan. Prosesnya mungkin agak lama karena Storybook install banyak dependencies sekaligus.

Setelah instalasi selesai, Storybook bakal generate beberapa file konfigurasi:

.storybook/
├── main.ts
└── preview.ts
src/stories/
├── assets/
├── Button.stories.ts
├── Button.tsx
├── Configure.mdx
├── Header.stories.ts
├── Header.tsx
├── Introduction.mdx
├── Page.stories.ts
└── Page.tsx

Kita bisa hapus file-file example di folder src/stories/ karena kita bakal bikin stories sendiri untuk component BuildWithAngga UI kita:

rm -rf src/stories

Update file .storybook/main.ts supaya sesuai dengan struktur project kita:

import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-onboarding',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },
}

export default config

Konfigurasi ini ngasih tau Storybook untuk cari stories di mana pun dalam folder src/ dengan pattern *.stories.*. TypeScript configuration juga di-enable supaya Storybook bisa extract prop information dari component kita.

Update juga .storybook/preview.ts untuk setup global styles:

import type { Preview } from '@storybook/react'
import '../src/styles/globals.css'

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    docs: {
      toc: true,
    },
  },
}

export default preview

File ini import global CSS yang kita buat sebelumnya, jadi semua component di Storybook bakal punya styling yang konsisten.

Membuat Story untuk Button Component

Sekarang kita bikin story untuk Button component. Buat file src/components/Button/Button.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react'
import Button from './Button'

const meta = {
  title: 'BuildWithAngga UI/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component: 'Button component yang flexible dengan berbagai variant dan state. Cocok untuk semua kebutuhan interaksi user di aplikasi BuildWithAngga.',
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
      description: 'Visual style dari button',
    },
    size: {
      control: { type: 'select' },
      options: ['sm', 'md', 'lg'],
      description: 'Ukuran button',
    },
    disabled: {
      control: { type: 'boolean' },
      description: 'Disable button interaction',
    },
    loading: {
      control: { type: 'boolean' },
      description: 'Show loading state dengan spinner',
    },
    fullWidth: {
      control: { type: 'boolean' },
      description: 'Button memenuhi lebar container',
    },
    iconPosition: {
      control: { type: 'select' },
      options: ['left', 'right'],
      description: 'Posisi icon relatif terhadap text',
    },
    onClick: {
      action: 'clicked',
      description: 'Callback ketika button diklik',
    },
  },
} satisfies Meta<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {
    children: 'Daftar Kelas Premium',
    variant: 'primary',
    size: 'md',
  },
}

export const Secondary: Story = {
  args: {
    children: 'Lihat Detail',
    variant: 'secondary',
    size: 'md',
  },
}

export const Outline: Story = {
  args: {
    children: 'Download Materi',
    variant: 'outline',
    size: 'md',
  },
}

export const Ghost: Story = {
  args: {
    children: 'Cancel',
    variant: 'ghost',
    size: 'md',
  },
}

export const Danger: Story = {
  args: {
    children: 'Hapus Akun',
    variant: 'danger',
    size: 'md',
  },
}

export const WithIcon: Story = {
  args: {
    children: 'Download',
    variant: 'primary',
    size: 'md',
    icon: '📥',
    iconPosition: 'left',
  },
}

export const Loading: Story = {
  args: {
    children: 'Menyimpan Progress',
    variant: 'primary',
    size: 'md',
    loading: true,
  },
}

export const Disabled: Story = {
  args: {
    children: 'Tidak Tersedia',
    variant: 'primary',
    size: 'md',
    disabled: true,
  },
}

export const FullWidth: Story = {
  args: {
    children: 'Gabung Sekarang',
    variant: 'primary',
    size: 'lg',
    fullWidth: true,
  },
  parameters: {
    layout: 'padded',
  },
}

export const AllSizes: Story = {
  args: {
    children: 'Button Text',
    variant: 'primary',
  },
  render: (args) => (
    <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
      <Button {...args} size="sm">
        Small
      </Button>
      <Button {...args} size="md">
        Medium
      </Button>
      <Button {...args} size="lg">
        Large
      </Button>
    </div>
  ),
}

export const AllVariants: Story = {
  args: {
    children: 'Button Text',
    size: 'md',
  },
  render: (args) => (
    <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
      <Button {...args} variant="primary">Primary</Button>
      <Button {...args} variant="secondary">Secondary</Button>
      <Button {...args} variant="outline">Outline</Button>
      <Button {...args} variant="ghost">Ghost</Button>
      <Button {...args} variant="danger">Danger</Button>
    </div>
  ),
}

Story file ini define berbagai scenarios untuk Button component. Setiap story represent use case yang berbeda, dari basic button sampai complex state kayak loading atau disabled.

Yang menarik adalah argTypes configuration yang memungkinkan user interact dengan component props lewat Storybook controls. User bisa ganti variant, size, atau toggle loading state secara real-time.

Preview Component di Browser

Sekarang jalankan Storybook development server:

npm run storybook

Storybook Button Component
Storybook Button Component

Command ini bakal start Storybook di http://localhost:6006. Browser bakal automatically open dan kamu bisa explore Button component dengan berbagai configurations.

Di Storybook interface, kamu bakal liat:

  1. Sidebar navigation - List semua stories yang tersedia
  2. Canvas tab - Preview component dengan interactive controls
  3. Docs tab - Auto-generated documentation dari TypeScript types
  4. Controls panel - Knobs untuk ubah props secara real-time
  5. Actions panel - Log dari event handlers kayak onClick

Yang keren dari Storybook adalah auto-generated documentation. Karena kita pake TypeScript dengan proper prop types, Storybook automatically extract information dan generate docs yang comprehensive.

Kamu juga bisa test accessibility, responsive behavior, dan berbagai edge cases langsung dari Storybook interface. Ini sangat membantu development process dan quality assurance.

Untuk build static version dari Storybook (untuk deploy ke hosting):

npm run build-storybook

Command ini generate static files di folder storybook-static/ yang bisa di-deploy ke GitHub Pages, Netlify, atau hosting manapun.

Dengan Storybook, documentation component library kita jadi interactive dan user-friendly. Developer yang mau pake library bisa langsung explore semua features tanpa harus setup project sendiri.

Persiapan Publish ke NPM

Sekarang kita masuk ke tahap yang exciting banget - mempersiapkan library kita untuk di-publish ke NPM registry. Ini kayak nyiapin produk untuk dijual di marketplace global. NPM adalah tempat dimana jutaan developer cari package untuk project mereka, jadi kita harus pastiin everything perfect sebelum upload.

Setup Package.json yang Benar

Package.json itu kayak kartu identitas dari library kita. File ini ngasih tau NPM dan user semua informasi penting tentang package kita. Sebelum publish, kita perlu double-check semua field udah benar dan complete.

Mari kita review dan update package.json yang udah kita buat sebelumnya:

{
  "name": "@withcakfan/buildwithangga-ui",
  "version": "0.0.1",
  "description": "Modern React component library untuk BuildWithAngga projects. Menyediakan component UI yang konsisten dan reusable untuk development yang lebih cepat.",
  "keywords": [
    "react",
    "components",
    "ui",
    "buildwithangga",
    "typescript",
    "vite",
    "library",
    "design-system"
  ],
  "main": "./dist/buildwithangga-ui.cjs.js",
  "module": "./dist/buildwithangga-ui.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/buildwithangga-ui.es.js",
      "require": "./dist/buildwithangga-ui.cjs.js"
    },
    "./dist/buildwithangga-ui.css": "./dist/buildwithangga-ui.css"
  },
  "files": [
    "dist",
    "README.md",
    "CHANGELOG.md"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vite build && npm run build:types",
    "build:types": "tsc --project tsconfig.lib.json",
    "build:watch": "vite build --watch",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepublishOnly": "npm run test && npm run build"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@vitejs/plugin-react": "^4.0.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/user-event": "^14.4.3",
    "vitest": "^0.34.6",
    "jsdom": "^22.1.0"
  },
  "repository": {
    "type": "git",
    "url": "<https://github.com/work-bwa/ui-components.git>"
  },
  "bugs": {
    "url": "<https://github.com/work-bwa/ui-components/issues>"
  },
  "homepage": "<https://github.com/work-bwa/ui-components#readme>",
  "author": {
    "name": "Cakfan",
    "email": "[email protected]",
    "url": "<https://github.com/withcakfan>"
  },
  "license": "MIT",
  "engines": {
    "node": ">=16.0.0",
    "npm": ">=8.0.0"
  },
  "publishConfig": {
    "access": "public"
  }
}

Field-field yang crucial untuk NPM publish:

  • name: Harus unique di NPM registry. Scoped package (@username/package) recommended
  • version: Mengikuti semantic versioning (major.minor.patch)
  • description: Penjelasan singkat yang menarik untuk attract users
  • keywords: Membantu discoverability di NPM search
  • main, module, types: Entry points untuk berbagai environments
  • files: Whitelist file/folder yang di-include dalam package
  • peerDependencies: Dependencies yang harus diprovide user
  • repository, bugs, homepage: Links untuk documentation dan support
  • license: Lisensi package (MIT recommended untuk open source)
  • engines: Minimum version requirement untuk Node.js dan NPM
  • publishConfig: Configuration khusus untuk publishing

Script prepublishOnly akan automatically run sebelum publish untuk memastiin test pass dan build sukses.

Menentukan Versi (0.0.1)

Versioning itu important banget dalam software development. NPM menggunakan semantic versioning (semver) dengan format major.minor.patch:

  • Major (0.x.x): Breaking changes yang nggak backward compatible
  • Minor (x.1.x): New features yang backward compatible
  • Patch (x.x.1): Bug fixes yang backward compatible

Untuk initial release, kita mulai dengan version 0.0.1. Version 0.x.x indicates bahwa package masih dalam development phase dan API belum stable.

Kenapa mulai dari 0.0.1 bukan 1.0.0? Karena:

  • Version 0.x.x ngasih flexibility untuk breaking changes
  • User aware bahwa API masih bisa berubah
  • Lebih safe untuk experimentation dan feedback gathering
  • Industry standard untuk new packages

Setelah API stable dan production-ready, baru kita release version 1.0.0. Ini signal ke community bahwa package udah mature dan API nggak akan berubah drastis.

NPM punya command untuk manage versioning:

# Increment patch version (0.0.1 -> 0.0.2)
npm version patch

# Increment minor version (0.0.1 -> 0.1.0)
npm version minor

# Increment major version (0.0.1 -> 1.0.0)
npm version major

# Set specific version
npm version 0.0.1

Command ini automatically update package.json dan create git tag jika project punya git repository.

Login ke NPM Account

Sebelum bisa publish, kita harus login ke NPM account. Pastiin kamu udah punya account di npmjs.com dengan username yang sama dengan scoped package name.

Login lewat terminal:

npm login

Command ini bakal prompt untuk:

  • Username: Harus sama dengan scope di package name (@withcakfan)
  • Password: Password NPM account kamu
  • Email: Email yang terdaftar di NPM
  • OTP: One-time password dari authenticator app (jika 2FA enabled)

Verify login status:

npm whoami

Terminal: npm whoami
Terminal: npm whoami

Command ini harusnya return username kamu jika login sukses.

Check current registry:

npm config get registry

Pastiin registry pointing ke https://registry.npmjs.org/ bukan private registry.

Untuk extra security, enable two-factor authentication di NPM account:

npm profile enable-2fa auth-and-writes

Ini akan require OTP untuk login dan publishing, which is highly recommended untuk protect package dari unauthorized access.

Test publishing dengan dry run:

npm publish --dry-run

Terminal: npm publish —dry-run
Terminal: npm publish —dry-run

Command ini simulate publishing process tanpa actually upload package. Berguna untuk check apa aja yang bakal di-include dalam package dan detect potential issues sebelum real publish.

Output harusnya show:

  • List files yang akan di-upload
  • Package size
  • Tarball contents
  • Any warnings atau errors

Kalo ada warning tentang repository URL format, jalankan command ini untuk fix:

npm pkg fix

NPM akan automatically correct format issues di package.json. Repository URL harusnya dalam format https://github.com/user/repo.git tanpa prefix git+.

Kalo semua check passed dan nggak ada error, berarti kita ready untuk actual publishing step!

Publish ke NPM

Ini dia moment yang udah kita tunggu-tunggu - publish library kita ke NPM registry! Rasanya kayak launching produk pertama ke dunia. Setelah semua persiapan selesai, sekarang saatnya bikin library BuildWithAngga UI tersedia untuk developer di seluruh dunia.

Perintah Publish Pertama Kali

Sebelum jalanin command publish, pastiin sekali lagi bahwa semua udah ready. Double check project kamu dengan checklist ini:

  • Build berhasil tanpa error (npm run build)
  • Test semua pass (npm test)
  • Package.json udah lengkap dan benar
  • Login ke NPM account (npm whoami return username kamu)
  • Dry run publish sukses (npm publish --dry-run)

Kalo semua udah green, saatnya publish for real:

npm publish

Command sederhana ini bakal trigger proses yang cukup complex di background. NPM akan:

  1. Run script prepublishOnly (test + build)
  2. Create tarball dari files yang di-specify di files array
  3. Upload tarball ke NPM registry
  4. Update NPM database dengan package metadata
  5. Make package available untuk di-install globally

Kalo ini publish pertama kali untuk scoped package (@withcakfan/buildwithangga-ui), kamu mungkin perlu specify access level:

npm publish --access public

By default, scoped package di-treat sebagai private (berbayar). Flag --access public memastiin package bisa di-access semua orang secara gratis.

Output yang sukses bakal terlihat kayak gini:

npm notice
npm notice 📦  @withcakfan/[email protected]
npm notice === Tarball Contents ===
npm notice 1.2kB dist/index.d.ts
npm notice 15.6kB dist/buildwithangga-ui.es.js
npm notice 12.1kB dist/buildwithangga-ui.cjs.js
npm notice 2.5kB dist/buildwithangga-ui.css
npm notice 1.8kB README.md
npm notice === Tarball Details ===
npm notice name:          @withcakfan/buildwithangga-ui
npm notice version:       0.0.1
npm notice filename:      withcakfan-buildwithangga-ui-0.0.1.tgz
npm notice package size:  12.3 kB
npm notice unpacked size: 33.2 kB
npm notice shasum:        a1b2c3d4e5f6...
npm notice integrity:     sha512-AbCdEfGh...
npm notice total files:   5
npm notice
+ @withcakfan/[email protected]

Congratulations! Package kamu sekarang udah live di NPM registry. Ini achievement yang luar biasa, especially untuk first-time publisher.

Kalo ada error, NPM biasanya kasih message yang cukup descriptive. Common errors include:

  • Package name udah exists (try different name)
  • Authentication failed (re-login dengan npm login)
  • Invalid package.json (fix dengan npm pkg fix)
  • Missing required fields (add to package.json)

Cek Package di NPMjs.com

Setelah publish sukses, langsung aja buka browser dan check package kamu di NPMjs.com:

<https://www.npmjs.com/package/@withcakfan/buildwithangga-ui>

@withcakfan/buildwithangga-ui - npm
@withcakfan/buildwithangga-ui - npm

Di halaman package, kamu bakal liat:

  • Package name dan version
  • Description dan keywords
  • Installation instructions
  • README content (kalo ada)
  • Dependencies information
  • Download statistics
  • Repository links

NPM automatically extract information dari package.json dan README.md untuk bikin halaman yang informatif. Ini kenapa penting banget untuk punya package.json yang complete dan README yang descriptive.

Test juga install package kamu di project lain untuk memastiin everything works:

# Di project terpisah
npm install @withcakfan/buildwithangga-ui

# Test import
import { Button } from '@withcakfan/buildwithangga-ui'
import '@withcakfan/buildwithangga-ui/dist/buildwithangga-ui.css'

Kalo install dan import berhasil tanpa error, berarti package kamu sudah benar-benar ready for production use!

Package page di NPMjs.com juga show download statistics yang bakal update daily. Ini useful untuk track adoption dan popularity package kamu over time.

Update Versi untuk Publish Ulang

Setelah initial publish, pasti akan ada saatnya kamu perlu update package dengan bug fixes, new features, atau improvements. NPM nggak allow publish dengan version yang sama, jadi kamu harus increment version number.

Workflow untuk publish update:

  1. Make changes di code atau documentation
  2. Test thoroughly untuk memastiin nggak ada regression
  3. Update version sesuai dengan type of changes
  4. Publish dengan version yang baru

Untuk increment version, gunakan NPM built-in commands:

# Untuk bug fixes (0.0.1 -> 0.0.2)
npm version patch

# Untuk new features (0.0.1 -> 0.1.0)
npm version minor

# Untuk breaking changes (0.0.1 -> 1.0.0)
npm version major

Command ini automatic update package.json dan create git tag (kalo project punya git repo). Tag berguna untuk track releases dan memudahkan rollback kalo needed.

Atau kalo mau set specific version:

npm version 0.0.2

Setelah version update, publish kayak biasa:

npm publish

NPM akan detect version change dan allow publish yang baru. Package page di NPMjs.com automatic update dengan version terbaru.

Best practices untuk versioning:

  • Patch versions untuk bug fixes yang nggak change API
  • Minor versions untuk new features yang backward compatible
  • Major versions untuk breaking changes yang require user code changes
  • Always update CHANGELOG.md untuk document changes
  • Use semantic versioning consistently

Example changelog entry:

## [0.0.2] - 2024-01-15

### Fixed
- Fix loading spinner animation di Button component
- Resolve CSS import issue di test environment

### Added
- Add size prop documentation di Storybook

Dengan workflow ini, kamu bisa maintain package yang reliable dan well-documented. Users akan appreciate clear versioning dan detailed changelog untuk understand apa yang berubah di setiap release.

Remember, publishing ke NPM itu commitment untuk maintain package dan support community. Tapi it's also rewarding experience yang bisa boost career dan contribute ke open source ecosystem!

Penutup

Selamat! Kamu udah berhasil membuat dan publish component library React pertama ke NPM registry. Ini pencapaian yang luar biasa dan menunjukkan bahwa kamu udah naik level sebagai developer. Perjalanan dari setup project sampai publish ke NPM memang nggak gampang, tapi kamu udah berhasil melewatinya dengan baik.

Apa yang Udah Kita Capai

Dalam tutorial ini, kita udah belajar banyak hal:

  • Setup project React dengan Vite dan TypeScript
  • Bikin component dengan proper props dan styling
  • Implementasi testing dengan Vitest dan React Testing Library
  • Konfigurasi build untuk production
  • Buat dokumentasi dengan Storybook
  • Publish package ke NPM registry

Component library yang kita buat ini bukan cuma sekedar project tutorial, tapi skill dunia nyata yang sangat berharga di industri. Banyak perusahaan yang butuh developer yang bisa bikin dan maintain design system atau component library internal.

Skill yang kamu kembangkan di sini bisa diterapkan untuk:

  • Frontend developer roles di startup atau perusahaan
  • Posisi design system engineer
  • Kontribusi open source
  • Project freelance yang butuh reusable components
  • Project personal yang berskala

Langkah Selanjutnya

Component library kita baru punya satu component (Button), tapi ini baru permulaan. Kamu bisa expand dengan component lain kayak Input, Card, Modal, Dropdown, dan masih banyak lagi. Setiap component baru adalah kesempatan untuk practice dan improve skill.

Tantangan untuk kedepannya:

  • Tambah lebih banyak component ke library
  • Implementasi theming system yang fleksibel
  • Tambah animasi dan micro-interactions
  • Optimasi bundle size dan performa
  • Buat dokumentasi website yang komprehensif
  • Bangun komunitas around library kamu

Industri terus berkembang dan demand untuk reusable components semakin tinggi. Skill yang kamu kembangkan di sini akan terus relevan dan berharga untuk pengembangan karir.

Terus Belajar dengan Mentor Berpengalaman

Membuat component library ini cuma sebagian kecil dari ecosystem React yang luas. Masih banyak topik advanced yang bisa di-explore lebih dalam dengan bimbingan yang tepat.

Di BuildWithAngga, kamu bisa lanjutin perjalanan belajar dengan:

Real Project Experience - Nggak cuma teori, tapi hands-on practice dengan project yang benar-benar digunakan di industri. Kamu bakal belajar dari case study nyata dan best practices yang terbukti di production environment.

Mentor Berpengalaman - Belajar langsung dari senior developer yang udah bertahun-tahun di industri. Mereka bisa share insight, troubleshooting tips, dan saran karir yang nggak bisa didapat dari tutorial biasa.

Akses Selamanya - Investasi dalam pengembangan skill itu long-term. Dengan akses selamanya, kamu bisa revisit materi, stay updated dengan perubahan terbaru, dan lanjutin belajar sesuai pace kamu sendiri.

Community Support - Connect dengan sesama learner yang punya tujuan yang sama. Sharing knowledge, kolaborasi di project, dan membangun network yang berharga untuk career growth.

Kurikulum yang Relevan dengan Industri - Materi yang di-design berdasarkan kebutuhan aktual di job market. Fokus pada skill yang benar-benar dibutuhkan di workplace, bukan cuma teknologi yang trending.

Component library yang kita buat hari ini adalah stepping stone untuk project yang lebih kompleks dan menantang. Terus bangun, terus belajar, dan yang paling penting, terus sharing knowledge dengan komunitas.

Perjalanan kamu sebagai developer baru dimulai. Component library pertama ini akan jadi portfolio piece yang bisa kamu showcase ke potential employers atau clients. Tapi lebih dari itu, ini adalah fondasi untuk continuous learning dan pertumbuhan.

Selamat coding, dan sampai jumpa di project berikutnya!