| name | init-mock-app |
| description | Initialize a minimal hello-world mock-app with React, Vite, TypeScript, Tailwind CSS, React Router, and MSW. Use when scaffolding a new mock-app from scratch, bootstrapping a project skeleton, or starting a fresh prototype. Produces a working app with routing, mock API support, and a hello-world landing page. |
Skill: Initialize a Hello-World Mock App
Scaffold a complete, production-shaped project that mirrors the existing mock-app/ stack. The result is a hello-world landing page that is fully wired for routing, mock APIs, Storybook, E2E testing, and the shared model/ layer — so every other skill works out of the box.
When to Use
- Creating a brand-new project from scratch
- Bootstrapping a prototype or proof-of-concept
- Setting up a second app alongside the existing one
When NOT to Use
- Adding a feature to the existing
mock-app/ — use the implement-feature skill instead
- Updating model types only — use the update-data-model skill
Tech Stack
| Layer | Tool | Version |
|---|
| Framework | React | 18 |
| Language | TypeScript | 5 |
| Bundler | Vite | 5 |
| Styling | Tailwind CSS (Vite plugin) | 4 |
| Routing | React Router DOM | 6 |
| Mock API | MSW (Mock Service Worker) | 2 |
| Model types | Zod + @model path alias | 3 |
| Unit testing | Vitest + Testing Library | 3 / 16 |
| E2E testing | Playwright | 1 |
| Component dev | Storybook | 8 |
| Sample data | @anatine/zod-mock + Faker | — |
| Dev tooling | Convey overlay | — |
Complete Directory Structure
Replace <app-dir> with the desired name (default: mock-app). This is the full tree of files to create.
<project-root>/
├── .gitignore
├── .devcontainer/
│ ├── devcontainer.json
│ └── Dockerfile
├── .github/
│ ├── copilot-instructions.md
│ └── skills/
│ └── component-registry/
│ └── REGISTRY.md
├── .vscode/
│ ├── extensions.json
│ ├── mcp.json
│ ├── settings.json
│ └── tasks.json
├── package.json
├── vitest.config.ts
├── playwright.config.ts
├── model/
│ ├── README.md
│ ├── index.ts
│ └── enums.ts
├── wiki/
│ └── product-management/
│ └── features/
│ └── .gitkeep
└── <app-dir>/
├── index.html
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
├── .storybook/
│ ├── main.ts
│ └── preview.ts
├── public/
│ └── (mockServiceWorker.js — generated by MSW)
├── e2e/
│ └── smoke.spec.ts
└── src/
├── main.tsx
├── App.tsx
├── App.css
├── index.css
├── test-setup.ts
├── vite-env.d.ts
├── components/
│ ├── Layout.tsx
│ ├── Navigation.tsx
│ ├── Navigation.stories.tsx
│ └── ui/
│ ├── index.ts
│ ├── Button.tsx
│ ├── Button.stories.tsx
│ ├── Input.tsx
│ ├── Input.stories.tsx
│ ├── Textarea.tsx
│ ├── Textarea.stories.tsx
│ ├── Badge.tsx
│ └── Badge.stories.tsx
├── features/
│ └── .gitkeep
├── pages/
│ ├── HomePage.tsx
│ └── AboutPage.tsx
└── mocks/
├── browser.ts
└── handlers.ts
Step-by-Step Instructions
Step 1 — Root-level files
.gitignore
Use the template at templates/.gitignore.
package.json
Use the template at templates/package.json.
Replace every occurrence of mock-app with <app-dir> if the user chose a different name.
vitest.config.ts
Use the template at templates/vitest.config.ts.
Replace mock-app with <app-dir> if needed.
playwright.config.ts
Use the template at templates/playwright.config.ts.
Replace mock-app with <app-dir> if needed.
Step 2 — .devcontainer/ (Codespaces / container support)
.devcontainer/Dockerfile
Use the template at templates/Dockerfile.
Installs Playwright chromium for both @playwright/test and @playwright/mcp (via playwright-core), ensuring E2E tests and the Playwright MCP server both work out of the box in containers.
.devcontainer/devcontainer.json
Use the template at templates/devcontainer.json.
Replace mock-app with <app-dir> if needed.
Forwards ports 3333 (Convey), 5173 (Vite), and 6006 (Storybook). Sets PLAYWRIGHT_MCP_HEADLESS and PLAYWRIGHT_MCP_NO_SANDBOX remote environment variables for reliable headless Playwright MCP operation in containers.
Step 3 — .vscode/ config
.vscode/extensions.json
{
"recommendations": [
"ms-playwright.playwright",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
]
}
.vscode/mcp.json
{
"servers": {
"playwright": {
"command": "npx",
"args": [
"-y",
"@playwright/mcp@latest",
"--browser",
"chromium",
"--no-sandbox",
"--caps=vision"
]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"${workspaceFolder}"
]
},
"convey": {
"type": "stdio",
"command": "npx",
"args": ["@bitovi/convey"],
"cwd": "${workspaceFolder}/<app-dir>"
}
}
}
.vscode/settings.json
{
"playwright.testDir": "<app-dir>/e2e",
"playwright.reuseBrowser": true,
"playwright.showTrace": false,
"testing.automaticallyOpenPeekView": "failureInVisibleDocument",
"testing.openTesting": "neverOpen",
"testing.automaticallyOpenTestResults": "neverOpen",
"files.associations": {
"*.spec.ts": "typescript"
},
"search.exclude": {
"**/node_modules": true,
"**/playwright-report": true,
"**/test-results": true
}
}
.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Run App (Dev Server)",
"type": "shell",
"command": "npm run dev",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^(.+)\\((\\d+,\\d+)\\):\\s+(error|warning)\\s+(TS\\d+):\\s+(.+)$",
"file": 1,
"location": 2,
"severity": 3,
"code": 4,
"message": 5
},
"background": {
"activeOnStart": true,
"beginsPattern": "VITE.*ready in",
"endsPattern": "(Local|Network):.*"
}
},
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false
},
"group": { "kind": "build", "isDefault": false }
},
{
"label": "Run Storybook",
"type": "shell",
"command": "npm run storybook",
"isBackground": true,
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false
}
},
{
"label": "Build App",
"type": "shell",
"command": "npm run build",
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false
},
"group": { "kind": "build", "isDefault": true }
},
{
"label": "Build Storybook",
"type": "shell",
"command": "npm run build-storybook",
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false
}
}
]
}
Step 4 — model/ directory
model/README.md
# Data Model
This directory contains all domain entity definitions using Zod schemas.
## Structure
- `enums.ts` — shared enums
- `Entity.ts` — Zod schema + TypeScript type per entity
- `Entity.sample.ts` — sample data generators per entity
- `index.ts` — barrel re-exports
## Adding a New Entity
Use the **update-data-model** skill.
model/enums.ts
model/index.ts
export * from './enums';
Step 5 — <app-dir>/ files
<app-dir>/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
if (location.hostname === 'localhost') {
const s = document.createElement('script');
s.src = 'http://localhost:3333/overlay.js';
document.head.appendChild(s);
}
</script>
</body>
</html>
<app-dir>/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'path';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@model': path.resolve(__dirname, '../model'),
},
},
});
<app-dir>/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,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"@model": ["../model/index"],
"@model/*": ["../model/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
<app-dir>/tsconfig.node.json
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
<app-dir>/src/vite-env.d.ts
<app-dir>/src/test-setup.ts
Use the template at templates/test-setup.ts.
<app-dir>/src/index.css
@import "tailwindcss";
<app-dir>/src/App.css
<app-dir>/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
async function enableMocking() {
if (typeof import.meta.env !== 'undefined' && import.meta.env.MODE !== 'development') {
return;
}
const { worker } = await import('./mocks/browser');
return worker.start({
onUnhandledRequest: 'bypass',
serviceWorker: {
url: '/mockServiceWorker.js',
},
});
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
<app-dir>/src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Layout from './components/Layout';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import './App.css';
function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
export default App;
<app-dir>/src/components/Navigation.tsx
import { Link, NavLink } from 'react-router-dom';
export interface NavItem {
label: string;
to: string;
}
export interface NavigationProps {
brand?: string;
items?: NavItem[];
}
const defaultItems: NavItem[] = [
{ label: 'Home', to: '/' },
{ label: 'About', to: '/about' },
];
export function Navigation({ brand = 'My App', items = defaultItems }: NavigationProps) {
return (
<header className="bg-white border-b border-slate-200 px-6 py-4">
<nav className="flex items-center gap-6">
<Link to="/" className="text-lg font-semibold text-slate-800 mr-4">
{brand}
</Link>
{items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
isActive
? 'text-sm font-medium text-blue-600'
: 'text-sm font-medium text-slate-600 hover:text-slate-900'
}
>
{item.label}
</NavLink>
))}
</nav>
</header>
);
}
<app-dir>/src/components/Navigation.stories.tsx
import { MemoryRouter } from 'react-router-dom';
import type { Meta, StoryObj } from '@storybook/react';
import { Navigation } from './Navigation';
const meta = {
title: 'Components/Navigation',
component: Navigation,
tags: ['autodocs'],
decorators: [
(Story) => (
<MemoryRouter initialEntries={['/']}>
<Story />
</MemoryRouter>
),
],
} satisfies Meta<typeof Navigation>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const CustomBrand: Story = {
args: { brand: 'Acme Corp' },
};
export const CustomItems: Story = {
args: {
items: [
{ label: 'Home', to: '/' },
{ label: 'Dashboard', to: '/dashboard' },
{ label: 'Settings', to: '/settings' },
],
},
};
<app-dir>/src/components/Layout.tsx
import type { ReactNode } from 'react';
import { Navigation } from './Navigation';
interface LayoutProps {
children: ReactNode;
}
export default function Layout({ children }: LayoutProps) {
return (
<div className="min-h-screen bg-slate-50">
<Navigation />
<main className="p-6">{children}</main>
</div>
);
}
<app-dir>/src/components/ui/
Copy all files from templates/ui/ into <app-dir>/src/components/ui/, excluding Modal.tsx and Modal.stories.tsx:
index.ts — barrel export for all UI components (omit Modal export)
Button.tsx + Button.stories.tsx — variants (primary, secondary, outline, ghost, destructive), sizes (sm, md, lg), loading state, forwardRef
Input.tsx + Input.stories.tsx — label, error/helper text, accessible IDs, forwardRef
Textarea.tsx + Textarea.stories.tsx — label, error/helper text, accessible IDs, forwardRef
Badge.tsx + Badge.stories.tsx — variants (default, success, warning, danger, info), sizes (sm, md, lg)
Update index.ts to remove the Modal export line.
<app-dir>/src/pages/AboutPage.tsx
export default function AboutPage() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800 mb-4">About</h1>
<p className="text-slate-600">This is the about page. Add your content here.</p>
</div>
);
}
<app-dir>/src/pages/HomePage.tsx
The home page serves as a design system showcase so developers can immediately see all available UI components:
import { useState } from 'react';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Textarea } from '../components/ui/Textarea';
import { Badge } from '../components/ui/Badge';
export default function HomePage() {
const [inputValue, setInputValue] = useState('');
const [textareaValue, setTextareaValue] = useState('');
return (
<div className="max-w-3xl space-y-10">
<div>
<h1 className="text-3xl font-bold text-slate-800 mb-2">Design System</h1>
<p className="text-slate-600">A showcase of available UI components.</p>
</div>
{/* Buttons */}
<section>
<h2 className="text-lg font-semibold text-slate-700 mb-4">Button</h2>
<div className="flex flex-wrap gap-3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
</div>
</section>
{/* Badges */}
<section>
<h2 className="text-lg font-semibold text-slate-700 mb-4">Badge</h2>
<div className="flex flex-wrap gap-3">
<Badge>Default</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
<Badge size="sm">Small</Badge>
<Badge size="lg">Large</Badge>
</div>
</section>
{/* Input */}
<section>
<h2 className="text-lg font-semibold text-slate-700 mb-4">Input</h2>
<div className="space-y-4 max-w-sm">
<Input
label="Default"
placeholder="Enter text..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Input label="With helper text" placeholder="you@example.com" helperText="We'll never share your email." />
<Input label="With error" value="bad input" error="This field is invalid" readOnly />
<Input label="Disabled" value="Cannot edit" disabled />
</div>
</section>
{/* Textarea */}
<section>
<h2 className="text-lg font-semibold text-slate-700 mb-4">Textarea</h2>
<div className="space-y-4 max-w-sm">
<Textarea
label="Default"
placeholder="Enter text..."
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
/>
<Textarea label="With helper text" helperText="Max 500 characters." />
<Textarea label="With error" value="bad input" error="This field is required" readOnly />
<Textarea label="Disabled" value="Cannot edit" disabled />
</div>
</section>
</div>
);
}
<app-dir>/src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
<app-dir>/src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/health', () => {
return HttpResponse.json({ status: 'ok' });
}),
];
Step 6 — Storybook config
<app-dir>/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-links',
'@bitovi/convey/storybook-addon',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
<app-dir>/.storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/index.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
Step 7 — E2E smoke test
<app-dir>/e2e/smoke.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Smoke Test', () => {
test('homepage loads with design system heading', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: /design system/i })).toBeVisible();
});
test('about page loads', async ({ page }) => {
await page.goto('/about');
await expect(page.getByRole('heading', { name: /about/i })).toBeVisible();
});
});
Step 8 — Support files
.github/copilot-instructions.md
Create a minimal version using the copilot-instructions template as a starting point. Replace <app-dir> with the actual directory name.
.github/skills/component-registry/REGISTRY.md
# Component Registry
## Extracted Components
### Button
- **Path**: `/mock-app/src/components/ui/Button.tsx`
- **Description**: Reusable button with variants (primary, secondary, outline, ghost, destructive) and sizes (sm, md, lg). Includes loading state, disabled state, full TypeScript types, and Storybook stories.
### Badge
- **Path**: `/mock-app/src/components/ui/Badge.tsx`
- **Description**: Inline label/tag with variants (default, success, warning, danger, info) and sizes (sm, md, lg). Includes Storybook stories.
### Input
- **Path**: `/mock-app/src/components/ui/Input.tsx`
- **Description**: Text input with optional label, error message, and helper text. Accessible IDs, `forwardRef`. Includes Storybook stories.
### Textarea
- **Path**: `/mock-app/src/components/ui/Textarea.tsx`
- **Description**: Textarea with optional label, error message, and helper text. Accessible IDs, `forwardRef`. Includes Storybook stories.
### Navigation
- **Path**: `/mock-app/src/components/Navigation.tsx`
- **Description**: Top-of-page nav bar with configurable brand name and nav items. Uses React Router `NavLink` for active-link highlighting. Includes Storybook stories.
## Unextracted Patterns
*None identified yet.*
wiki/product-management/features/.gitkeep
Empty file — placeholder to keep the directory in git.
<app-dir>/src/features/.gitkeep
Empty file — placeholder for feature modules.
Step 9 — Install dependencies
npm install
npm install --save-dev @bitovi/convey
@bitovi/convey must be installed as a local dev dependency so Storybook can resolve the @bitovi/convey/storybook-addon. The MCP server itself runs via npx so no global install is needed.
Step 10 — Generate the MSW service worker
npx msw init <app-dir>/public --save
Step 11 — Install Playwright browsers
npx playwright install chromium
Step 12 — Verify everything works
npm run dev
npm test
npm run storybook
npm run test:e2e
Skills Compatibility Checklist
This scaffold is designed so every project skill works immediately:
| Skill | What it needs | Provided by scaffold |
|---|
| component-registry | src/components/ui/, REGISTRY.md | ✅ ui/ with Button, Input, Textarea, Badge + Navigation + pre-populated REGISTRY.md |
| create-skill | .github/skills/ directory | ✅ Exists via component-registry |
| debug-e2e-test | e2e/ dir, playwright.config.ts | ✅ Both created |
| document-feature | wiki/product-management/features/, model/ | ✅ Both created |
| extract-ui-component | src/components/ui/, Storybook config | ✅ ui/ with components + .storybook/ |
| generate-sample-data | model/, @anatine/zod-mock, @faker-js/faker | ✅ All present |
| implement-feature | src/features/, all of the above | ✅ All present |
| responsive-design | Tailwind, Playwright | ✅ Both configured |
| update-data-model | model/ with index.ts, enums.ts, README.md | ✅ All created |
| write-e2e-test | e2e/, playwright.config.ts | ✅ Both created |
Customization Points
After scaffolding, the user will likely want to:
- Add pages: Create files in
src/pages/, add <Route> entries in App.tsx, add items to the defaultItems array in Navigation.tsx
- Add mock API handlers: Edit
src/mocks/handlers.ts with new http.get()/http.post() entries
- Import model types:
import type { ... } from '@model' — path alias is pre-configured
- Add a login flow: Wrap routing in auth state similar to
mock-app/src/App.tsx
- Add domain entities: Use the update-data-model skill to create
model/Entity.ts + model/Entity.sample.ts
Common Mistakes to Avoid
- Using Tailwind v3 directives — Tailwind v4 uses
@import "tailwindcss" instead of @tailwind base/components/utilities. No tailwind.config.js or postcss.config.js needed.
- Skipping
npx msw init — MSW needs its service worker file in public/
- Defining types outside
/model — Always use the @model alias; never create local interfaces for domain entities
- Missing
.storybook/preview.ts CSS import — Storybook won't render Tailwind classes without it
- Not installing Playwright browsers —
npx playwright install chromium is required before running E2E tests
- Forgetting
vitest.config.ts — Unit tests won't run without the root-level Vitest config
- Missing
test-setup.ts — Testing Library matchers (.toBeInTheDocument() etc.) require the setup file importing @testing-library/jest-dom/vitest
- Omitting
@tailwindcss/vite plugin — Tailwind v4 requires the Vite plugin in vite.config.ts instead of PostCSS config