Swerk

Swerk / Oliwer Code Style

Last active 3 hours ago

Like 0

Mon style de code que je partage.

Revision 5da5d420349faca48c006213155dac7a225d73c3

SKILL.md Raw

name: oliwer-code-style description: > Applique le style de code personnel d'Oliwer pour tout projet web TypeScript/JavaScript (React, Next.js, Vite, Hono, Prisma).

TOUJOURS utiliser ce skill quand :

  • Tu generes du code TS/JS/TSX/JSX (composants React, hooks, services, controleurs, routes, etc.)
  • Tu reviewes ou corriges du code existant TypeScript/JavaScript
  • Tu proposes une architecture de projet, une structure de dossiers, ou un decoupage de modules
  • Tu travailles sur un projet avec Hono, Prisma, Next.js, Vite, ou React
  • L'utilisateur demande comment structurer une feature, un module, un service, ou un composant

Style de Code Personnel JS/TS

Dev expert. Respecte conventions strictes pour tout projet web (React, NextJS, Vite, Hono).


0. Regles de Style du Texte Genere

Ces regles s'appliquent a tout texte produit par Claude : commentaires de code, messages d'erreur, JSDoc, logs, prose.

  • Apostrophes : Ne jamais ecrire ' (apostrophe typographique ou droite) dans du texte genere. Utiliser ' a la place. Exemples : J'ai, l'utilisateur, n'existe pas, c'est.
  • Tiret cadratin : Ne jamais utiliser -- ou --- comme separateur dans du texte. Utiliser : ou reformuler la phrase.
  • Ces regles ne s'appliquent pas au contenu des blocs de code (les strings JS/TS, les valeurs, les templates litteraux gardent leur syntaxe normale).
// ❌ A eviter dans commentaires et JSDoc
// Valide l'email — retourne null si invalide
// J'ai choisi cette approche car...
 
// ✅ Correct
// Valide l'email. Retourne null si invalide.
// J'ai choisi cette approche car...
 
// Les strings dans le code restent normales
const msg = "L'email est invalide"  // OK, c'est du code

1. Architecture et Paradigme

  • General : Fonctions flechees (const myFunc = () => {}) pour composants React, utilitaires, logique fonctionnelle.
  • Frontend (Hors NextJS) : Architecture "Features" : code groupe par fonctionnalite, pas par type de fichier.
  • API/Backend (Hono/Prisma) : OO (Classes) pour controleurs/services. Structure modulaire par ressource.
  • Server/Client (Next.js) : Tout composant = Server Component par defaut. 'use client' uniquement si hooks, evenements DOM ou state local. Jamais sur layout sauf necessite absolue.
// ✅ Structure backend modulaire (Hono)
src/
├── modules/
│   └── user/
│       ├── user.controller.ts   # Classe : orchestration requete/reponse
│       ├── user.service.ts      # Classe : logique metier pure
│       ├── user.repository.ts   # Classe : acces Prisma
│       ├── user.routes.ts       # Fonctions flechees : declaration routes Hono
│       └── index.ts             # Barrel export
├── middlewares/
└── index.ts
 
// ✅ Structure feature-based (React/Vite)
src/
├── features/
│   └── auth/
│       ├── components/
│       ├── hooks/
│       ├── utils/
│       └── index.ts
├── components/ui/
└── lib/

2. Imports et Exports

  • Barrel files (index.ts) toujours. Importer depuis barrel, jamais depuis fichier direct.
// ✅ Bien
import { UserService, AuthService } from '@/modules/user'
import { Button, Input } from '@/components/ui'
 
// ❌ Eviter
import { UserService } from '@/modules/user/user.service'
import { Button } from '@/components/ui/Button'

3. Typage TypeScript et Validation

  • any INTERDIT. Generiques ou unknown si necessaire.
  • interface pour objets extensibles. type pour unions/intersections/alias primitifs.
  • enum pour valeurs fixes connues a la compilation.
  • Zod pour toute validation runtime (formulaires, body, API externes, env vars).
// ✅ interface pour les objets
interface User {
  id: string
  email: string
  role: UserRole
}
 
// ✅ type pour les unions
type Status = 'active' | 'inactive' | 'pending'
 
// ✅ enum pour les valeurs fixes
enum UserRole {
  Admin = 'ADMIN',
  Member = 'MEMBER',
  Guest = 'GUEST',
}
 
// ✅ Zod pour validation + inference de type
const CreateUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  role: z.nativeEnum(UserRole).default(UserRole.Member),
})
type CreateUserDto = z.infer<typeof CreateUserSchema>
 
// ✅ Variables d'environnement typees et validees
// src/env.ts
const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
})
export const env = EnvSchema.parse(process.env)
// Usage : import { env } from '@/env'

4. Gestion des Erreurs

  • Backend (Hono) : Middleware d'erreur global + classes typees heritant Error.
  • Frontend : Retourner objet discrimine depuis Server Actions/fetch. Pas de throw cote client.
  • Async/await toujours. Jamais .then()/.catch(). try/catch explicite.
// ✅ Classes d'erreurs typees (backend)
class AppError extends Error {
  constructor(
    public readonly message: string,
    public readonly statusCode: number,
    public readonly code: string,
  ) {
    super(message)
  }
}
class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND')
  }
}
 
// ✅ Resultat discrimine (frontend / Server Actions)
type Result<T> = { success: true; data: T } | { success: false; error: string }
 
const createUser = async (dto: CreateUserDto): Promise<Result<User>> => {
  try {
    const user = await userService.create(dto)
    return { success: true, data: user }
  } catch (error) {
    return { success: false, error: "Impossible de creer l'utilisateur." }
  }
}
 
// Cote composant
const result = await createUser(formData)
if (!result.success) {
  setError(result.error)
  return
}
redirect(`/users/${result.data.id}`)

5. Conventions de Nommage

  • PascalCase : composants, classes, interfaces, enums + fichiers (UserCard.tsx, UserService.ts).
  • camelCase : hooks, utilitaires, variables, fonctions + fichiers (useAuth.ts, formatDate.ts).
  • kebab-case : fichiers/dossiers routeur Next.js (mon-profil/page.tsx, user-settings/layout.tsx).
  • SCREAMING_SNAKE_CASE : constantes globales (MAX_RETRY_COUNT, API_BASE_URL).
// ✅ Nommage correct
src/
├── app/                          # Next.js App Router
│   └── user-settings/
│       └── page.tsx              # kebab-case
├── features/auth/
│   ├── components/
│   │   └── LoginForm.tsx         # PascalCase
│   ├── hooks/
│   │   └── useSession.ts         # camelCase
│   └── utils/
│       └── formatAuthError.ts    # camelCase
└── lib/
    └── constants.ts              # MAX_UPLOAD_SIZE = 10_000_000

6. Performance React

  • React Compiler (experimental.reactCompiler: true) : Pas de useMemo/useCallback/React.memo manuel : le compiler insere automatiquement.
  • Sans compiler (React <=18) : useMemo/useCallback uniquement si calcul couteux (profile) ou lib tierce compare par reference (DnD, chart, map).
  • Preferer composition a memorisation manuelle.
// ✅ Avec React Compiler : ecriture naturelle, le compiler optimise
const ProductList = ({ products, onSelect }: ProductListProps) => {
  const sorted = products.slice().sort((a, b) => a.name.localeCompare(b.name))
  const handleSelect = (id: string) => onSelect(id)
  return <ul>{sorted.map(p => <ProductItem key={p.id} product={p} onSelect={handleSelect} />)}</ul>
}
 
// ✅ Sans compiler : memorisation manuelle ciblee apres profiling
const ProductList = ({ products, onSelect }: ProductListProps) => {
  const sorted = useMemo(
    () => products.slice().sort((a, b) => a.name.localeCompare(b.name)),
    [products],
  )
  const handleSelect = useCallback((id: string) => onSelect(id), [onSelect])
  return <ul>{sorted.map(p => <ProductItem key={p.id} product={p} onSelect={handleSelect} />)}</ul>
}

7. Hooks Personnalises

  • Prefixe use + PascalCase : useUserSession, useCartItems.
  • 1 hook = 1 responsabilite. Extraire des que ~3 hooks natifs lies au meme domaine.
  • Dans src/hooks/ (global) ou src/features/<name>/hooks/ (local) + barrel export.
// ✅ useCartItems.ts
const useCartItems = () => {
  const [items, setItems] = useState<CartItem[]>([])
  const [isLoading, setIsLoading] = useState(false)
 
  const addItem = async (productId: string) => {
    setIsLoading(true)
    const result = await cartService.add(productId)
    if (result.success) setItems(prev => [...prev, result.data])
    setIsLoading(false)
  }
 
  const removeItem = (id: string) => {
    setItems(prev => prev.filter(item => item.id !== id))
  }
 
  return { items, isLoading, addItem, removeItem }
}

8. UI et Composants (React / NextJS)

  • Tailwind CSS exclusivement. Pas de styles inline sauf valeurs dynamiques impossibles en classes.
  • Variantes conditionnelles complexes : clsx ou cva.
  • Verifier si composant existe avant d'en creer un. Tout reutilisable dans components/ui/.
// ✅ Variantes avec cva
import { cva, type VariantProps } from 'class-variance-authority'
import { clsx } from 'clsx'
 
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        ghost: 'bg-transparent hover:bg-neutral-100 text-neutral-700',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'h-8 px-3',
        md: 'h-10 px-4',
        lg: 'h-12 px-6',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  },
)
 
interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}
 
const Button = ({ variant, size, className, ...props }: ButtonProps) => (
  <button className={clsx(buttonVariants({ variant, size }), className)} {...props} />
)

9. Qualite de Code (Clean Code)

  • SRP : 1 fonction/composant = 1 tache.
  • DRY : Extraire logique recurrente en helpers, services ou hooks.
  • Early Returns : Cas d'erreur en premier, reduit l'indentation.
  • Nommage descriptif : Noms longs/clairs, pas d'abreviations.
// ❌ Mauvais : pas d'early return, nommage opaque, logique melangee
const proc = async (d: unknown) => {
  if (d) {
    const parsed = schema.safeParse(d)
    if (parsed.success) {
      const u = await db.user.findUnique({ where: { email: parsed.data.email } })
      if (u) {
        return u
      }
    }
  }
  return null
}
 
// ✅ Bien : early returns, nommage clair, responsabilite unique
const findUserByEmail = async (rawInput: unknown): Promise<User | null> => {
  if (!rawInput) return null
 
  const parseResult = FindUserSchema.safeParse(rawInput)
  if (!parseResult.success) return null
 
  const user = await db.user.findUnique({ where: { email: parseResult.data.email } })
  return user ?? null
}

10. Commentaires et Documentation

  • Zero fichier de doc separe (README.md, docs/) sauf demande explicite.
  • JSDoc sur classes, interfaces complexes, fonctions critiques : explique le "pourquoi", pas le "quoi".
  • Pas de commentaires qui paraphrasent le code.
// ❌ Commentaire inutile : paraphrase le code
// Incremente le compteur de 1
counter++
 
// ✅ JSDoc utile : explique le "pourquoi" et le contrat
/**
 * Valide et normalise un numero SIRET francais.
 * Utilise l&apos;algorithme de Luhn modifie requis par l&apos;INSEE.
 *
 * @param siret - Le numero SIRET brut (espaces toleres)
 * @returns Le SIRET normalise sans espaces, ou null si invalide
 */
const validateSiret = (siret: string): string | null => { ... }

11. Ecosysteme et Outillage

  • bun en priorite. Repli pnpm si necessaire. Prettier + ESLint gerent le formatage.
  • Recherche dans le code : Utiliser rg (ripgrep) plutot que grep. Plus rapide, respecte .gitignore par defaut, supporte les types de fichiers.
# ✅ Commandes standard
bun install
bun dev
bun run build
bun test
 
# Repli pnpm si necessaire
pnpm install
pnpm dev
 
# ✅ Recherche avec ripgrep
rg "useCartItems" src/
rg -t ts "interface User" src/
rg --no-heading -n "TODO" src/
 
# ❌ Eviter grep pour chercher dans le code source
grep -r "useCartItems" src/

12. Infrastructure et Deploiement (Docker)

  • Jamais root dans conteneur. Utilisateur dedie non-root, permissions minimales.
  • Multi-stage build pour minimiser taille image finale.
# ✅ Dockerfile securise avec multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN npm install -g bun && bun install --frozen-lockfile
COPY . .
RUN bun run build
 
FROM node:20-alpine AS runner
WORKDIR /app
 
# Creation d'un utilisateur non-root dedie
RUN addgroup --system --gid 1001 appgroup \
  && adduser --system --uid 1001 --ingroup appgroup appuser
 
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
 
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]