frontend/vuejs-project-starter/SKILL.md
Scaffold a Vue 3.5+ project with Composition API (`<script setup>`), Pinia stores, Vue Router 4, TypeScript, Vite, and auto-imports via unplugin.
npx skillsauth add achreftlili/deep-dev-skills vuejs-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold a Vue 3.5+ project with Composition API (
<script setup>), Pinia stores, Vue Router 4, TypeScript, Vite, and auto-imports via unplugin.
npm create vue@latest my-app
# Select: TypeScript, Vue Router, Pinia, ESLint, Prettier
cd my-app
npm install
npm install -D unplugin-auto-import unplugin-vue-components
npm install tailwindcss @tailwindcss/vite
src/
├── app/
│ ├── App.vue # Root component
│ └── router.ts # Vue Router configuration
├── features/
│ ├── auth/
│ │ ├── components/ # Feature-specific components
│ │ ├── composables/ # Feature-specific composables
│ │ ├── stores/ # Feature-specific Pinia stores
│ │ ├── services/ # API calls for this feature
│ │ ├── types.ts # Feature-specific types
│ │ └── index.ts # Barrel export
│ ├── dashboard/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ └── index.ts
│ └── settings/
│ ├── components/
│ └── index.ts
├── shared/
│ ├── components/ # Reusable UI components
│ ├── composables/ # Generic reusable composables
│ ├── utils/ # Pure utility functions
│ └── types/ # Global shared types
├── assets/ # Static assets
├── styles/
│ └── main.css # Tailwind CSS entry point
└── main.ts # App entry point
.env.example # Required env vars template
<script setup> everywhere: use the <script setup lang="ts"> syntax for all components. No Options API.use* composable functions. These replace mixins entirely.unplugin-auto-import and unplugin-vue-components to avoid manual imports of Vue APIs and components."strict": true in tsconfig.json.defineProps<T>(): use the type-based declaration for full TypeScript support.defineEmits<T>(): explicitly type all component events.vite.config.ts)import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { fileURLToPath } from "url";
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
AutoImport({
imports: ["vue", "vue-router", "pinia"],
dts: "src/auto-imports.d.ts",
}),
Components({
dirs: ["src/shared/components"],
dts: "src/components.d.ts",
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
src/styles/main.css)@import "tailwindcss";
src/app/router.ts)import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
redirect: "/dashboard",
},
{
path: "/login",
name: "login",
component: () => import("@/features/auth/components/LoginPage.vue"),
},
{
path: "/dashboard",
name: "dashboard",
component: () => import("@/features/dashboard/components/DashboardPage.vue"),
meta: { requiresAuth: true },
},
{
path: "/settings",
name: "settings",
component: () => import("@/features/settings/components/SettingsPage.vue"),
meta: { requiresAuth: true },
},
],
});
// Navigation guard
router.beforeEach((to) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { name: "login" };
}
});
export default router;
src/main.ts)import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "@/app/App.vue";
import router from "@/app/router";
import "@/styles/main.css";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");
src/app/App.vue)<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template>
<RouterView />
</template>
<!-- src/features/dashboard/components/UserCard.vue -->
<script setup lang="ts">
interface User {
id: string;
name: string;
email: string;
}
const props = defineProps<{
user: User;
selected?: boolean;
}>();
const emit = defineEmits<{
select: [userId: string];
delete: [userId: string];
}>();
</script>
<template>
<div
class="rounded border p-3"
:class="{ 'border-blue-500 bg-blue-50': selected }"
@click="emit('select', user.id)"
>
<p class="font-medium">{{ user.name }}</p>
<p class="text-sm text-gray-500">{{ user.email }}</p>
<button
class="mt-2 text-sm text-red-600 hover:underline"
@click.stop="emit('delete', user.id)"
>
Delete
</button>
</div>
</template>
// src/features/auth/stores/authStore.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { authService } from "@/features/auth/services/authService";
interface User {
id: string;
email: string;
name: string;
}
export const useAuthStore = defineStore("auth", () => {
const user = ref<User | null>(null);
const token = ref<string | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);
const isAuthenticated = computed(() => user.value !== null);
async function login(email: string, password: string) {
isLoading.value = true;
error.value = null;
try {
const response = await authService.login({ email, password });
user.value = response.user;
token.value = response.token;
} catch (err) {
error.value = err instanceof Error ? err.message : "Login failed";
throw err;
} finally {
isLoading.value = false;
}
}
function logout() {
user.value = null;
token.value = null;
}
return { user, token, isLoading, error, isAuthenticated, login, logout };
});
// src/shared/composables/useFetch.ts
import { ref, watchEffect, type Ref } from "vue";
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<string | null>;
isLoading: Ref<boolean>;
refetch: () => Promise<void>;
}
export function useFetch<T>(url: string | Ref<string>): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<string | null>(null);
const isLoading = ref(false);
async function fetchData() {
isLoading.value = true;
error.value = null;
try {
const resolvedUrl = typeof url === "string" ? url : url.value;
const response = await fetch(resolvedUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (err) {
error.value = err instanceof Error ? err.message : "Fetch failed";
} finally {
isLoading.value = false;
}
}
watchEffect(() => {
fetchData();
});
return { data, error, isLoading, refetch: fetchData };
}
<!-- src/features/dashboard/components/DashboardPage.vue (Provider) -->
<script setup lang="ts">
import { provide, ref } from "vue";
import type { InjectionKey, Ref } from "vue";
export interface Theme {
primary: string;
secondary: string;
}
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
const theme = ref<Theme>({
primary: "#3b82f6",
secondary: "#64748b",
});
provide(ThemeKey, theme);
</script>
<template>
<div>
<slot />
</div>
</template>
<!-- Child component that injects the theme -->
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey, type Theme } from "@/features/dashboard/components/DashboardPage.vue";
// Provide a default value to avoid undefined — inject() returns T | undefined without one
const theme = inject(ThemeKey, ref<Theme>({ primary: "#000000", secondary: "#666666" }));
</script>
<template>
<div :style="{ color: theme.primary }">
Themed content
</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from "vue";
const searchQuery = ref("");
const selectedId = ref<string | null>(null);
// Watch a specific ref
watch(searchQuery, (newVal, oldVal) => {
console.log(`Search changed: "${oldVal}" -> "${newVal}"`);
});
// Watch with options
watch(selectedId, async (id) => {
if (id) {
await fetchUserDetails(id);
}
}, { immediate: true });
// watchEffect — auto-tracks dependencies
watchEffect(() => {
document.title = searchQuery.value
? `Search: ${searchQuery.value}`
: "My App";
});
</script>
<!-- Render modal content at document body level -->
<script setup lang="ts">
const showModal = ref(false);
</script>
<template>
<button @click="showModal = true">Open Modal</button>
<Teleport to="body">
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="rounded bg-white p-6 shadow-lg">
<h2 class="mb-4 text-lg font-bold">Modal Title</h2>
<p>Modal content goes here.</p>
<button @click="showModal = false" class="mt-4 rounded bg-blue-600 px-4 py-2 text-white">
Close
</button>
</div>
</div>
</Teleport>
</template>
<!-- src/features/dashboard/components/DashboardPage.vue -->
<script setup lang="ts">
import { useDashboardStore } from "@/features/dashboard/stores/dashboardStore";
import UserCard from "./UserCard.vue";
const store = useDashboardStore();
onMounted(() => {
store.fetchUsers();
});
const searchQuery = ref("");
const filteredUsers = computed(() =>
store.users.filter((u) =>
u.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
);
</script>
<template>
<main class="p-6">
<h1 class="mb-4 text-2xl font-bold">Dashboard</h1>
<input
v-model="searchQuery"
type="text"
placeholder="Search users..."
class="mb-4 rounded border px-3 py-2"
/>
<div class="space-y-2">
<UserCard
v-for="user in filteredUsers"
:key="user.id"
:user="user"
@select="store.selectUser"
@delete="store.deleteUser"
/>
</div>
<p v-if="filteredUsers.length === 0" class="text-gray-500">
No users found.
</p>
</main>
</template>
.env.example to .env and fill in valuesnpm installnpm run devnpm run type-check to confirm TypeScript is clean# Development
npm run dev # Start dev server (http://localhost:5173)
# Build
npm run build # Type check + Vite production build
npm run preview # Preview production build
# Lint & Format
npm run lint # Run ESLint
npm run format # Run Prettier
# Type check
npm run type-check # Vue TSC type checking (vue-tsc --noEmit)
npm install -D vitest @vue/test-utils jsdom). The official Vue testing library.npm install @tanstack/vue-query).npm install vee-validate) + Zod for schema-based form validation.vue-i18n for internationalization.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.