CtrlK
BlogDocsLog inGet started
Tessl Logo

vuejs-project-starter

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-starter
What are skills?

Overall
score

17%

Does it follow best practices?

Validation for skill structure

Validation failed for this skill
This skill has errors that need to be fixed before it can move to Implementation and Discovery review.
SKILL.md
Review
Evals

Vue.js Project Starter

Scaffold a Vue 3.5+ project with Composition API (<script setup>), Pinia stores, Vue Router 4, TypeScript, Vite, and auto-imports via unplugin.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x (or pnpm >= 9.x)
  • Git

Scaffold Command

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

Project Structure

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

Key Conventions

  • <script setup> everywhere: use the <script setup lang="ts"> syntax for all components. No Options API.
  • Composables for logic reuse: extract reusable logic into use* composable functions. These replace mixins entirely.
  • Pinia for global state: one store per feature/domain. Use the Setup Store syntax (function-based) for full TypeScript inference.
  • Auto-imports: use unplugin-auto-import and unplugin-vue-components to avoid manual imports of Vue APIs and components.
  • Feature-based organization: same principle as other framework skills. Features are self-contained folders.
  • TypeScript strict mode: enable "strict": true in tsconfig.json.
  • Props defined with defineProps<T>(): use the type-based declaration for full TypeScript support.
  • Emits defined with defineEmits<T>(): explicitly type all component events.

Essential Patterns

Vite Config with Auto-imports (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)),
    },
  },
});

Tailwind CSS Entry (src/styles/main.css)

@import "tailwindcss";

Router with Lazy Loading (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;

App Entry (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");

Root Component (src/app/App.vue)

<script setup lang="ts">
import { RouterView } from "vue-router";
</script>

<template>
  <RouterView />
</template>

Component with Props and Emits

<!-- 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>

Pinia Store (Setup Syntax)

// 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 };
});

Composable (Reusable Logic)

// 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 };
}

Provide / Inject Pattern

<!-- 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>

Watchers

<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>

Teleport (Portal)

<!-- 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>

Page Component (Putting It Together)

<!-- 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>

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: npm install
  3. Start dev server: npm run dev
  4. Verify the app loads at http://localhost:5173
  5. Run npm run type-check to confirm TypeScript is clean

Common Commands

# 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)

Integration Notes

  • Testing: use Vitest + Vue Test Utils (npm install -D vitest @vue/test-utils jsdom). The official Vue testing library.
  • E2E: Playwright or Cypress. Both integrate well with Vue.
  • State management: Pinia is the official store. For server-state caching, add TanStack Query for Vue (npm install @tanstack/vue-query).
  • UI components: Tailwind handles styling. For prebuilt components, consider Radix Vue, PrimeVue, or Naive UI.
  • Forms: VeeValidate (npm install vee-validate) + Zod for schema-based form validation.
  • API layer: pair with an API client skill. Axios or a typed fetch wrapper works well with Pinia actions.
  • Auth: the Pinia store + router navigation guard pattern shown above is the standard approach.
  • i18n: add vue-i18n for internationalization.
Repository
github.com/achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.