Scaffold a Vue 3.5+ project with Composition API (`<script setup>`), Pinia stores, Vue Router 4, TypeScript, Vite, and auto-imports via unplugin.
Install with Tessl CLI
npx tessl i github:achreftlili/deep-dev-skills --skill vuejs-project-starterOverall
score
17%
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
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/vitesrc/
├── 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.If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.