Saltar a contenido

Svelte / SvelteKit - Guía de Entorno

Guía para configurar el entorno de desarrollo Svelte/SvelteKit para trabajar con Claude Code.

Resumen de Capacidades

Capacidad Herramientas
Framework Svelte 5, SvelteKit 2
Build Vite
State Svelte Stores, Runes
Testing Vitest, Playwright
Styling Tailwind, vanilla CSS
Type Safety TypeScript

Instalación

Node.js y pnpm

# Windows
winget install OpenJS.NodeJS.LTS
corepack enable
corepack prepare pnpm@latest --activate

# macOS
brew install node@22
corepack enable

# Verificar
node --version   # 22.x+
pnpm --version   # 9.x+

Crear Proyectos

SvelteKit (Full-stack - Recomendado)

# Crear proyecto
pnpm create svelte@latest my-sveltekit-app

# Opciones recomendadas:
# - Which template: Skeleton project
# - TypeScript: Yes
# - ESLint: Yes
# - Prettier: Yes
# - Playwright: Yes
# - Vitest: Yes

cd my-sveltekit-app
pnpm install
pnpm dev

Svelte Standalone (Vite)

# Solo frontend (sin SSR)
pnpm create vite@latest my-svelte-app -- --template svelte-ts

cd my-svelte-app
pnpm install
pnpm dev

Estructura de Proyecto (SvelteKit)

my-sveltekit-app/
├── src/
│   ├── lib/
│   │   ├── components/
│   │   │   └── Button.svelte
│   │   ├── stores/
│   │   │   └── user.ts
│   │   ├── server/
│   │   │   └── db.ts         # Solo server-side
│   │   └── utils/
│   │       └── helpers.ts
│   ├── routes/
│   │   ├── +layout.svelte
│   │   ├── +layout.server.ts
│   │   ├── +page.svelte
│   │   ├── +page.ts          # Load data
│   │   ├── +page.server.ts   # Server-only load
│   │   ├── about/
│   │   │   └── +page.svelte
│   │   └── api/
│   │       └── users/
│   │           └── +server.ts
│   ├── app.html
│   ├── app.css
│   └── app.d.ts
├── static/
├── tests/
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
└── package.json

Svelte 5 Runes

Svelte 5 introduce "runes" como nueva API de reactividad.

$state (Estado reactivo)

<script lang="ts">
  // Svelte 5 runes
  let count = $state(0);
  let user = $state({ name: 'John', age: 30 });

  function increment() {
    count++;  // Automáticamente reactivo
  }

  function updateName(newName: string) {
    user.name = newName;  // Mutación directa OK
  }
</script>

<button onclick={increment}>
  Count: {count}
</button>

<p>User: {user.name}, {user.age}</p>

$derived (Valores computados)

<script lang="ts">
  let count = $state(0);

  // Automáticamente recalculado cuando count cambia
  let doubled = $derived(count * 2);

  // Derived con lógica más compleja
  let message = $derived.by(() => {
    if (count > 10) return 'High';
    if (count > 5) return 'Medium';
    return 'Low';
  });
</script>

<p>Count: {count}, Doubled: {doubled}, Level: {message}</p>

$effect (Side effects)

<script lang="ts">
  let count = $state(0);

  // Se ejecuta cuando count cambia
  $effect(() => {
    console.log('Count changed:', count);

    // Cleanup (opcional)
    return () => {
      console.log('Cleanup');
    };
  });

  // Pre-effect (antes del DOM update)
  $effect.pre(() => {
    console.log('Before DOM update');
  });
</script>

$props (Props del componente)

<!-- Button.svelte -->
<script lang="ts">
  interface Props {
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
    onclick?: () => void;
    children: import('svelte').Snippet;
  }

  let {
    variant = 'primary',
    disabled = false,
    onclick,
    children
  }: Props = $props();
</script>

<button
  class={variant}
  {disabled}
  {onclick}
>
  {@render children()}
</button>
<!-- Uso -->
<Button variant="primary" onclick={() => alert('Clicked!')}>
  Click me
</Button>

Routing (SvelteKit)

Páginas

src/routes/
├── +page.svelte           → /
├── about/
│   └── +page.svelte       → /about
├── blog/
│   ├── +page.svelte       → /blog
│   └── [slug]/
│       └── +page.svelte   → /blog/:slug
└── users/
    └── [...rest]/
        └── +page.svelte   → /users/*

Load Functions

// routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params, fetch }) => {
  const response = await fetch(`/api/posts/${params.slug}`);
  const post = await response.json();

  return {
    post
  };
};
<!-- routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>

<article>
  <h1>{data.post.title}</h1>
  <div>{@html data.post.content}</div>
</article>

Server Load (con acceso a DB)

// routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { redirect } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  const stats = await db.getStats(locals.user.id);

  return {
    stats
  };
};

API Routes

// routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get('limit')) || 10;
  const users = await db.getUsers(limit);
  return json(users);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();

  if (!body.email) {
    throw error(400, 'Email is required');
  }

  const user = await db.createUser(body);
  return json(user, { status: 201 });
};

Stores (Svelte 4 / compatible con 5)

// lib/stores/user.ts
import { writable, derived } from 'svelte/store';

interface User {
  id: string;
  name: string;
  email: string;
}

function createUserStore() {
  const { subscribe, set, update } = writable<User | null>(null);

  return {
    subscribe,
    login: (user: User) => set(user),
    logout: () => set(null),
    updateName: (name: string) =>
      update(u => u ? { ...u, name } : null)
  };
}

export const user = createUserStore();

// Derived store
export const isAuthenticated = derived(user, $user => !!$user);
<script lang="ts">
  import { user, isAuthenticated } from '$lib/stores/user';
</script>

{#if $isAuthenticated}
  <p>Welcome, {$user?.name}</p>
  <button onclick={() => user.logout()}>Logout</button>
{:else}
  <a href="/login">Login</a>
{/if}

Testing

Unit Tests (Vitest)

// tests/components/Button.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from '$lib/components/Button.svelte';

describe('Button', () => {
  it('renders with text', () => {
    render(Button, { props: { children: 'Click me' } });
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onclick when clicked', async () => {
    const handleClick = vi.fn();
    render(Button, { props: { onclick: handleClick } });

    await fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalled();
  });
});
pnpm test
pnpm test:unit

E2E Tests (Playwright)

// tests/home.test.ts
import { expect, test } from '@playwright/test';

test('home page has welcome message', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('h1')).toContainText('Welcome');
});

test('navigation works', async ({ page }) => {
  await page.goto('/');
  await page.click('[data-testid="nav-about"]');
  await expect(page).toHaveURL('/about');
});
pnpm test:e2e
pnpm playwright test

Forms y Actions

<!-- routes/login/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  let { form }: { form: ActionData } = $props();
</script>

<form method="POST" use:enhance>
  <input name="email" type="email" required />
  <input name="password" type="password" required />

  {#if form?.error}
    <p class="error">{form.error}</p>
  {/if}

  <button type="submit">Login</button>
</form>
// routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { db } from '$lib/server/db';

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email') as string;
    const password = data.get('password') as string;

    const user = await db.authenticate(email, password);

    if (!user) {
      return fail(400, { error: 'Invalid credentials' });
    }

    cookies.set('session', user.sessionId, { path: '/' });
    throw redirect(303, '/dashboard');
  }
};

Styling

Global CSS

/* src/app.css */
:root {
  --primary: #ff3e00;
  --bg: #ffffff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a1a;
  }
}

Scoped Styles

<style>
  /* Automáticamente scoped al componente */
  .card {
    background: var(--bg);
    padding: 1rem;
    border-radius: 8px;
  }

  /* Global dentro de componente */
  :global(.external-lib-class) {
    color: var(--primary);
  }
</style>

Tailwind CSS

pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
  content: ['./src/**/*.{html,js,svelte,ts}'],
  theme: {
    extend: {}
  },
  plugins: []
};

Comandos que Claude Code Ejecutará

# Crear proyecto
pnpm create svelte@latest my-app
pnpm create vite@latest my-app -- --template svelte-ts

# Desarrollo
pnpm dev
pnpm build
pnpm preview

# Testing
pnpm test
pnpm test:unit
pnpm test:e2e

# Linting
pnpm lint
pnpm check        # svelte-check

# Dependencias
pnpm add package
pnpm add -D dev-package

VS Code Extensions

code --install-extension svelte.svelte-vscode
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension bradlc.vscode-tailwindcss

Verificación del Entorno

#!/bin/bash
echo "=== Verificación Entorno Svelte ==="

echo -e "\n--- Runtime ---"
node --version
pnpm --version

echo -e "\n--- Svelte ---"
pnpm dlx svelte --version 2>/dev/null || echo "Svelte via create-svelte"

echo -e "\n=== Verificación Completa ==="

Recursos