| name | init-frontend |
| description | Bootstrap a new frontend project with craft principles: React + Vite + TypeScript + Vitest + clean architecture |
| user-invocable | false |
| context | fork |
| agent | architect |
| allowed-tools | Read, Grep, Glob, Edit, Write, Bash |
Init Frontend Skill
You are bootstrapping a new frontend project following craftsmanship principles.
Project Setup Checklist
When initializing a new frontend project, follow these steps:
1. Create Project with Vite + React + TypeScript
npm create vite@latest PROJECT_NAME -- --template react-ts
cd PROJECT_NAME
npm install
2. Configure Strict TypeScript
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"],
"@lib/*": ["src/lib/*"],
"@types/*": ["src/types/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
3. Install Core Dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @vitest/coverage-v8
npm install @tanstack/react-query zod
npm install -D @types/node
4. Configure Vite with Path Aliases
Update vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@lib': path.resolve(__dirname, './src/lib'),
'@types': path.resolve(__dirname, './src/types'),
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/index.ts',
],
},
},
})
5. Setup Test Environment
Create src/test/setup.ts:
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
afterEach(() => {
cleanup()
})
6. Create Project Structure
src/
├── components/
│ ├── ui/ # Generic, reusable UI components
│ │ └── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ └── index.ts
│ └── features/ # Feature-specific components
│ └── .gitkeep
├── hooks/ # Custom React hooks
│ └── index.ts
├── lib/ # Utilities, helpers, API clients
│ ├── api/
│ │ └── client.ts
│ └── utils/
│ └── index.ts
├── types/ # Shared TypeScript types
│ ├── common.ts
│ └── index.ts
├── test/ # Test utilities and setup
│ ├── setup.ts
│ ├── test-utils.tsx
│ └── factories/
│ └── index.ts
├── App.tsx
├── main.tsx
└── index.css
7. Create Shared Types
Create src/types/common.ts:
export type Brand<T, B> = T & { __brand: B }
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
export type AsyncData<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E }
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value })
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error })
Create src/types/index.ts:
export * from './common'
8. Create Test Utilities
Create src/test/test-utils.tsx:
import { render, RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactElement, ReactNode } from 'react'
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
}
type WrapperProps = {
children: ReactNode
}
function AllProviders({ children }: WrapperProps) {
const queryClient = createTestQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, { wrapper: AllProviders, ...options })
}
export * from '@testing-library/react'
export { userEvent } from '@testing-library/user-event'
export { customRender as render }
9. Create Base UI Component Example
Create src/components/ui/Button/Button.tsx:
import { ButtonHTMLAttributes, forwardRef } from 'react'
type ButtonVariant = 'primary' | 'secondary' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
isLoading?: boolean
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
isLoading = false,
disabled,
children,
className = '',
...props
},
ref
) => {
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
}
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button
ref={ref}
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<>
<span className="sr-only">Loading...</span>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</>
) : null}
{children}
</button>
)
}
)
Button.displayName = 'Button'
Create src/components/ui/Button/Button.test.tsx:
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@/test/test-utils'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
describe('Button', () => {
it('should render children', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('should call onClick when clicked', async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should be disabled when isLoading is true', () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
it('should indicate loading state to screen readers', () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true')
})
it('should not call onClick when disabled', async () => {
const handleClick = vi.fn()
render(<Button disabled onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).not.toHaveBeenCalled()
})
})
Create src/components/ui/Button/index.ts:
export { Button } from './Button'
10. Update package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"typecheck": "tsc --noEmit"
}
}
11. Create .gitignore Additions
# Dependencies
node_modules
# Build
dist
dist-ssr
# IDE
.idea
.vscode/*
!.vscode/extensions.json
# Testing
coverage
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
# OS
.DS_Store
Thumbs.db
Post-Setup Verification
After setup, verify everything works:
npm run typecheck
npm run test:run
npm run dev
Optional Enhancements
Based on project needs, consider adding:
ESLint + Prettier
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react-hooks eslint-plugin-react-refresh prettier eslint-config-prettier
Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
React Router
npm install react-router-dom
MSW for API Mocking
npm install -D msw
Project Initialized Successfully
The project is now set up with:
- Strict TypeScript configuration
- Vitest with Testing Library
- React Query for server state
- Clean folder structure
- Path aliases for clean imports
- Base UI components with tests
- Type utilities (Result, AsyncData, Brand)