Back to flin
flin

La Règle d'Or : un fichier .flin suffit

La règle d'or de FLIN : un fichier .flin remplace 15 fichiers de configuration. Pas de package.json, pas de tsconfig, pas de webpack.

Thales & Claude | March 30, 2026 14 min flin
EN/ FR/ ES
flinzero-configsimplicityone-filedeveloper-experience

Pas de package.json. Pas de tsconfig.json. Pas de webpack.config.js. Pas de postcss.config.js. Pas de .eslintrc. Pas de .prettierrc. Pas de next.config.js. Pas de docker-compose.yml. Pas de .env.local. Pas de .env.production. Pas de jest.config.js. Pas de babel.config.js. Pas de tailwind.config.js. Pas de vite.config.ts.

Un fichier.

C'est la Règle d'Or de FLIN, et c'est la rupture la plus radicale avec le fonctionnement du développement web moderne. Dans cet article, nous allons construire une application web complète, avec base de données, entièrement réactive, dans un seul fichier .flin -- puis montrer l'équivalent en React/Next.js pour rendre le contraste viscéral.

L'état de l'art : une application todo en React/Next.js

Avant de montrer la version FLIN, soyons honnêtes sur ce que "construire une application todo" signifie en 2024 avec les outils standard de l'industrie.

Vous commencez par échafauder le projet :

bashnpx create-next-app@latest my-todo --typescript --tailwind --app --eslint
cd my-todo
npm install prisma @prisma/client
npx prisma init

Quatre commandes. Trois minutes si votre connexion internet est rapide. Plus longtemps si elle ne l'est pas. Quand la poussière retombe, votre répertoire de projet contient plus de 50 000 fichiers et pèse environ 400 méga-octets.

Vous devez ensuite créer le schéma de la base de données :

prisma// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id        Int      @id @default(autoincrement())
  title     String
  done      Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Lancer la migration :

bashnpx prisma migrate dev --name init
npx prisma generate

Créer la route d'API :

typescript// app/api/todos/route.ts
import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function GET() {
    const todos = await prisma.todo.findMany({
        orderBy: { createdAt: 'desc' }
    });
    return NextResponse.json(todos);
}

export async function POST(request: Request) {
    const body = await request.json();
    const todo = await prisma.todo.create({
        data: { title: body.title }
    });
    return NextResponse.json(todo, { status: 201 });
}

Créer la route d'API de suppression/mise à jour :

typescript// app/api/todos/[id]/route.ts
import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function PATCH(
    request: Request,
    { params }: { params: { id: string } }
) {
    const body = await request.json();
    const todo = await prisma.todo.update({
        where: { id: parseInt(params.id) },
        data: { done: body.done }
    });
    return NextResponse.json(todo);
}

export async function DELETE(
    request: Request,
    { params }: { params: { id: string } }
) {
    await prisma.todo.delete({
        where: { id: parseInt(params.id) }
    });
    return NextResponse.json({ success: true });
}

Créer le composant React :

tsx// app/page.tsx
'use client';

import { useState, useEffect } from 'react';

interface Todo {
    id: number;
    title: string;
    done: boolean;
}

export default function TodoApp() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [newTodo, setNewTodo] = useState('');
    const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

    useEffect(() => {
        fetch('/api/todos')
            .then(res => res.json())
            .then(setTodos);
    }, []);

    const addTodo = async () => {
        if (!newTodo.trim()) return;
        const res = await fetch('/api/todos', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: newTodo })
        });
        const todo = await res.json();
        setTodos([todo, ...todos]);
        setNewTodo('');
    };

    const toggleTodo = async (todo: Todo) => {
        const res = await fetch(`/api/todos/${todo.id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ done: !todo.done })
        });
        const updated = await res.json();
        setTodos(todos.map(t => t.id === updated.id ? updated : t));
    };

    const deleteTodo = async (id: number) => {
        await fetch(`/api/todos/${id}`, { method: 'DELETE' });
        setTodos(todos.filter(t => t.id !== id));
    };

    const filtered = todos.filter(todo => {
        if (filter === 'active') return !todo.done;
        if (filter === 'completed') return todo.done;
        return true;
    });

    return (
        <main className="max-w-md mx-auto p-4">
            <h1 className="text-2xl font-bold mb-4">Mes Todos</h1>
            <div className="flex gap-2 mb-4">
                <input
                    className="flex-1 border rounded px-3 py-2"
                    value={newTodo}
                    onChange={e => setNewTodo(e.target.value)}
                    onKeyDown={e => e.key === 'Enter' && addTodo()}
                    placeholder="Que faut-il faire ?"
                />
            </div>
            <nav className="flex gap-2 mb-4">
                <button onClick={() => setFilter('all')}>Tous</button>
                <button onClick={() => setFilter('active')}>Actifs</button>
                <button onClick={() => setFilter('completed')}>Faits</button>
            </nav>
            <ul>
                {filtered.map(todo => (
                    <li key={todo.id} className="flex items-center gap-2 py-1">
                        <input
                            type="checkbox"
                            checked={todo.done}
                            onChange={() => toggleTodo(todo)}
                        />
                        <span className={todo.done ? 'line-through' : ''}>
                            {todo.title}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>x</button>
                    </li>
                ))}
            </ul>
            <footer className="mt-4 text-sm text-gray-500">
                {todos.filter(t => !t.done).length} éléments restants
            </footer>
        </main>
    );
}

Comptons ce que cette application todo a nécessité :

Fichiers créés ou modifiés         Objectif
-------------------------------    ---------------------------
package.json                       Dépendances
prisma/schema.prisma               Schéma de base de données
.env                               DATABASE_URL
app/api/todos/route.ts             Endpoints GET et POST
app/api/todos/[id]/route.ts        Endpoints PATCH et DELETE
app/page.tsx                       Composant React

Six fichiers pour la logique applicative, plus quinze fichiers de configuration générés par create-next-app. Plus de 120 lignes de TypeScript. Une migration Prisma. Un fichier de base de données SQLite. Et environ 400 Mo de node_modules.

Pour une liste de tâches.

La version FLIN : un seul fichier

Voici la même application en FLIN :

flin// app.flin -- Application todo complète avec persistance en base de données

todos = []
filter = "all"
newTodo = ""

entity Todo {
    title: text
    done: bool = false
    created: time = now
}

filtered = match filter {
    "all"       -> Todo.all
    "active"    -> Todo.where(done == false)
    "completed" -> Todo.where(done == true)
}

<main>
    <h1>Mes Todos</h1>

    <input value={newTodo} placeholder="Que faut-il faire ?"
           enter={save Todo { title: newTodo }; newTodo = ""}>

    <nav>
        <button click={filter = "all"}>Tous</button>
        <button click={filter = "active"}>Actifs</button>
        <button click={filter = "completed"}>Faits</button>
    </nav>

    {for todo in filtered}
        <div class="todo-item">
            <input type="checkbox" checked={todo.done}
                   change={todo.done = !todo.done; save todo}>
            <span class={if todo.done then "done" else ""}>{todo.title}</span>
            <button click={delete todo}>x</button>
        </div>
    {/for}

    <footer>{Todo.where(done == false).count} éléments restants</footer>
</main>

Trente-trois lignes. Un fichier. Zéro configuration. Zéro dépendance. Zéro méga-octet de node_modules.

Sauvegardez-le sous app.flin. Lancez flin dev. L'application est en ligne.

La comparaison ligne par ligne

Parcourons ce que chaque section du code FLIN remplace.

Lignes 3-5 : gestion d'état.

flintodos = []
filter = "all"
newTodo = ""

Dans React, c'est useState appelé trois fois, avec des annotations de type explicites, des fonctions setter, et la charge de comprendre les règles des hooks. Dans FLIN, les variables sont déclarées et elles sont réactives. Le compilateur suit les dépendances.

Cela remplace : les hooks React, Redux, Zustand, ou toute autre bibliothèque de gestion d'état.

Lignes 7-11 : définition d'entité.

flinentity Todo {
    title: text
    done: bool = false
    created: time = now
}

Ce seul bloc définit simultanément : un type à la TypeScript, une table de base de données avec des colonnes, une migration automatique, des opérations CRUD (find, where, all, count) et un suivi d'historique.

Cela remplace : un schéma Prisma, un fichier de migration, une connexion à la base de données et un import du client Prisma.

Lignes 13-17 : valeurs calculées.

flinfiltered = match filter {
    "all"       -> Todo.all
    "active"    -> Todo.where(done == false)
    "completed" -> Todo.where(done == true)
}

La variable filtered se recalcule automatiquement chaque fois que filter change. C'est le système réactif de FLIN en action -- pas de useMemo, pas de tableaux de dépendances explicites, pas de gestion manuelle des abonnements.

Cela remplace : useMemo ou useEffect avec un tableau de dépendances, plus la logique Array.filter.

Ligne 21 : l'input avec gestionnaire enter.

flin<input value={newTodo} placeholder="Que faut-il faire ?"
       enter={save Todo { title: newTodo }; newTodo = ""}>

Une ligne qui fait cinq choses : lie la valeur de l'input à newTodo, gère l'événement touche Entrée, crée une nouvelle entité Todo, la sauvegarde en base de données et vide le champ input. Dans la version React, c'est un gestionnaire onKeyDown qui appelle une fonction async qui appelle fetch qui fait un POST à une route d'API qui appelle Prisma.

Cela remplace : un gestionnaire d'événement React, un appel fetch, une route d'API, une opération Prisma create et la logique de mise à jour d'état.

Lignes 27-33 : la liste de todos avec checkbox et suppression.

flin{for todo in filtered}
    <div class="todo-item">
        <input type="checkbox" checked={todo.done}
               change={todo.done = !todo.done; save todo}>
        <span class={if todo.done then "done" else ""}>{todo.title}</span>
        <button click={delete todo}>x</button>
    </div>
{/for}

Le gestionnaire de la checkbox bascule todo.done et sauvegarde l'entité en une seule expression. Le bouton supprimer retire l'entité de la base de données avec le mot-clé delete. Dans React, chacune de ces opérations nécessite une fonction async, un appel fetch, une route d'API, une opération Prisma et une réconciliation d'état.

Cela remplace : deux gestionnaires d'événements React, deux appels fetch, deux routes d'API (PATCH et DELETE), deux opérations Prisma et deux mises à jour d'état.

Ce que la règle du fichier unique signifie vraiment

La Règle d'Or n'est pas "vous devez tout mettre dans un seul fichier". FLIN supporte les projets multi-fichiers. La règle est : un seul fichier doit suffire.

L'application FLIN minimum viable est toujours un fichier. Vous pouvez grandir à partir de là :

// Étape 1 : Tout dans un fichier
app.flin

// Étape 2 : Pages séparées
index.flin
about.flin
products/index.flin
products/[id].flin

// Étape 3 : Extraire des composants
components/Header.flin
components/Footer.flin
components/ProductCard.flin

// Étape 4 : Séparer les entités (optionnel)
entities/User.flin
entities/Product.flin

// Étape 5 : Ajouter des routes d'API
api/users.flin
api/users/[id].flin

À chaque étape, il y a zéro fichier de configuration. La transition de l'étape 1 à l'étape 5 ne nécessite pas d'ajouter une bibliothèque de routage, un changement de configuration de build ou une migration de base de données. Vous créez simplement plus de fichiers .flin.

C'est la différence critique avec l'écosystème JavaScript, où chaque transition d'étape nécessite de nouveaux outils :

Étape                        L'écosystème JavaScript ajoute
-------------------------    -----------------------------------
Page unique                  React + config Vite
Pages multiples              React Router ou Next.js + sa config
Composants                   (déjà disponible, mais import/export nécessaires)
Base de données              Prisma + schéma + migration + .env
Routes d'API                 Express ou routes API Next.js + config
Build de production          Docker + Dockerfile + config de build

Chaque ligne ajoute de la configuration. Chaque ligne ajoute des dépendances. Chaque ligne ajoute des conflits de versions potentiels. Dans FLIN, chaque ligne ajoute des fichiers .flin. Rien d'autre.

Le coût cognitif de la configuration

Les fichiers de configuration ne sont pas simplement agaçants. Ils imposent un coût cognitif mesurable.

Chaque fichier de configuration dans un projet est un fichier qu'un développeur doit comprendre, ou au moins connaître, pour déboguer les problèmes. Quand un build échoue, le développeur doit vérifier : l'erreur est-elle dans mon code, dans la config Vite, dans la config TypeScript, dans la config PostCSS, ou dans une interaction entre elles ?

Ce coût cognitif est invisible parce qu'il est constant. Comme un bruit de fond, les développeurs cessent de le remarquer -- jusqu'à ce qu'il disparaisse.

Considérez le processus de débogage quand une classe CSS ne s'applique pas dans un projet Next.js :

1. La classe est-elle correcte ?                   (vérifier votre code)
2. Tailwind traite-t-il le fichier ?               (vérifier tailwind.config.js)
3. PostCSS fonctionne-t-il ?                       (vérifier postcss.config.js)
4. Le CSS est-il importé ?                         (vérifier l'import globals.css)
5. Le serveur dev utilise-t-il la bonne config ?   (vérifier next.config.js)
6. Y a-t-il un problème de cache ?                 (supprimer .next, redémarrer)

Six endroits à vérifier pour une classe CSS manquante. Dans FLIN, le processus de débogage est :

1. La classe est-elle correcte ?                   (vérifier votre code)

Un endroit. Parce qu'il y a un seul outil.

L'argument de la performance

La règle du fichier unique a des implications directes sur la performance.

Une application FLIN démarre en millisecondes parce qu'il n'y a pas de résolution de modules, pas de graphe de dépendances à construire, pas de fichiers de configuration à parser, pas de plugins à charger. Le compilateur FLIN lit un fichier .flin, le tokenise, le parse, vérifie les types et commence à servir -- le tout en une fraction du temps qu'il faut à Vite pour afficher son logo ASCII.

Le build de production est un binaire unique. Pas de node_modules à livrer. Pas de package.json pour spécifier une version de moteur. Pas de Dockerfile à écrire. Le binaire tourne sur la machine cible. C'est toute l'histoire du déploiement.

bash# Déploiement FLIN
flin build
scp app /server:/usr/local/bin/
ssh server 'app'

# Déploiement Next.js
npm run build
# ... configurer Docker
# ... écrire le Dockerfile
# ... construire l'image
# ... pousser vers le registre
# ... configurer l'orchestrateur
# ... déployer

La règle du fichier unique se répercute sur tout le cycle de vie. Développement plus simple. Builds plus simples. Déploiement plus simple. Débogage plus simple. Intégration des nouveaux plus simple. Chaque étape est plus simple parce que la fondation est plus simple.

Un exemple e-commerce dans un seul fichier

Pour prouver que la règle du fichier unique tient au-delà des exemples jouets, voici une page de liste de produits avec recherche sémantique, filtrage et panier :

flinentity Product {
    name: text
    description: semantic text
    price: money
    stock: int
    category: text
    image: file
}

entity CartItem {
    product: Product
    quantity: int = 1
}

searchQuery = ""
category = "all"
cart = CartItem.all

products = if searchQuery.len > 3 then
    search searchQuery in Product by description limit 20
else if category != "all" then
    Product.where(category == category)
else
    Product.all

cartTotal = cart.reduce(0, (sum, item) => sum + item.product.price * item.quantity)

<main>
    <header>
        <h1>Boutique</h1>
        <input placeholder="Rechercher des produits..." value={searchQuery}>
        <span class="cart-badge">{cart.count} articles - {cartTotal}</span>
    </header>

    <nav>
        <button click={category = "all"}>Tous</button>
        <button click={category = "electronics"}>Électronique</button>
        <button click={category = "clothing"}>Vêtements</button>
        <button click={category = "books"}>Livres</button>
    </nav>

    <div class="products">
        {for product in products}
            <div class="product-card">
                <img src={product.image.url}>
                <h3>{product.name}</h3>
                <p class="price">{product.price}</p>
                <p class="stock">{product.stock} en stock</p>
                <button click={save CartItem { product: product }}>
                    Ajouter au panier
                </button>
            </div>
        {/for}
    </div>
</main>

Cinquante-deux lignes. Deux entités (Product et CartItem). Recherche sémantique. Filtrage par catégorie. Un panier avec compteur d'articles et total. Le tout dans un fichier, avec zéro configuration, zéro dépendance et zéro outil de build.

L'équivalent React/Next.js nécessiterait : un schéma Prisma avec deux modèles, une migration, au moins trois routes d'API (liste de produits, recherche, opérations de panier), un composant React avec de multiples hooks, une solution de gestion d'état pour le panier, et un pipeline d'embeddings pour la recherche sémantique. De manière conservatrice, 300-400 lignes réparties sur 8-10 fichiers.

La Règle d'Or en pratique

La Règle d'Or -- un fichier .flin suffit -- n'est pas un slogan. C'est une contrainte de conception que nous appliquons rigoureusement.

Chaque nouvelle fonctionnalité proposée pour FLIN doit répondre à la question : "Est-ce que cela fonctionne toujours dans une application à fichier unique ?" Si une fonctionnalité nécessite de créer un deuxième fichier, un fichier de configuration ou un outil externe, elle viole la Règle d'Or.

Cette contrainte a façonné FLIN de façons qu'aucun autre principe de conception n'aurait pu :

  • Les entités sont en ligne. Vous les définissez dans le même fichier où vous les utilisez. Pas de fichier de schéma séparé requis.
  • Le routage est basé sur les fichiers. La structure de répertoires est le routeur. Pas de fichier de configuration du routeur.
  • Les types sont inférés. Pas de fichier de définition de types séparé. Pas de tsconfig.json.
  • Les tests sont en ligne. (Quand le lanceur de tests sera livré.) Pas de fichier de test séparé requis.
  • Les styles peuvent être en ligne. Pas de fichier CSS séparé requis pour les cas simples.

Le résultat est un langage où la distance entre l'idée et l'application fonctionnelle est un fichier et une commande. Pas cinq fichiers et douze commandes. Pas un processus d'échafaudage de vingt minutes. Un fichier. Une commande.

app.flin et flin dev. C'est toute l'expérience développeur.

Comme l'éléphant, FLIN porte tout ce dont il a besoin.


Ceci est la partie 5 de la série "Comment nous avons construit FLIN", qui documente comment un CEO à Abidjan et une IA CTO ont conçu et construit un langage de programmation qui remplace 47 technologies par une seule.

Navigation de la série : - [1] Pourquoi nous avons construit un langage de programmation en partant de zéro - [2] 47 technologies remplacées par un seul langage - [3] Nommer un langage d'après un éléphant : l'origine fongbé de FLIN - [4] Cinq principes de conception qui façonnent chaque ligne de FLIN - [5] La Règle d'Or : un fichier .flin suffit (vous êtes ici)

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles