speedster-js Guide

Guide d'utilisation

Le README explique pourquoi speedster-js est fait comme il est. Ce guide explique comment s'en servir. Pour essayer sans rien installer, le bac à sable compile et exécute un composant .spd dans le navigateur, avec six exemples et une URL partageable.

Mise en place Un projet qui démarre, ci-dessous
Composants Le langage .spd, référence complète
Réactivité Ce qui se met à jour, et pourquoi
Composition Propriétés, fragments, contenu transmis
Formulaires bind: sous toutes ses formes
Mouvement Transitions et actions
Internationalisation Messages, langues, formats
Accessibilité Ce qui est vérifié au build
Routeur Routes de fichiers, gabarits, fournisseurs
Rendu serveur SSR, hydratation, îlots
Outillage Build, types, éditeur
Rapport de build Poids, problèmes, accessibilité — à la demande

Mise en place

speedster-js s'appuie sur Bun pour le build. Rien n'y est spécifique au-delà du plugin : le compilateur produit du TypeScript ordinaire, et transform est exporté séparément pour brancher un autre empaqueteur.

Le plus court chemin :

bunx @ajustor/speedster-cli init mon-app && cd mon-app && bun install && bun run dev

speedster init écrit tout ce que les étapes 1 à 6 décrivent à la main, y compris les réglages qu'on oublie. Le reste de cette page explique ce qu'il produit.

0. Les paquets

Les paquets sont publiés sur npm, sous le scope @ajustor. Aucun jeton ni .npmrc n'est nécessaire.

Sans passer par npm. Pour développer le framework lui-même, ou consommer votre arbre de travail sans attendre une publication, bun link dans chaque paquet puis bun link @ajustor/speedster dans le projet consommateur suffit. Ni jeton ni tag.

1. Les dépendances

bun add @ajustor/speedster
bun add -d @types/bun typescript

Un seul nom à installer et à mettre à jour. @ajustor/speedster porte le runtime et l'outillage, et bun lie aussi les binaires de ses dépendances : speedster, speedster-check et speedster-i18n sont donc disponibles sans être installés à la main.

Ce paquet unique ne change rien au poids livré : bun run size compare les deux chemins d'import et les trouve identiques à l'octet près. Ce qui grossit, c'est node_modules, puisque l'outillage vient avec.

Les paquets détaillés restent publiés, si vous préférez ne prendre que le nécessaire :

bun add @ajustor/speedster-runtime
bun add -d @ajustor/speedster-plugin @ajustor/speedster-check @ajustor/speedster-cli

@ajustor/speedster-runtime est la seule dépendance d'exécution. Le compilateur, le plugin, le vérificateur et l'internationalisation tournent au build et ne partent jamais dans le navigateur.

1 bis. L'éditeur

La coloration, la complétion et les diagnostics des .spd viennent d'une extension VS Code, distribuée en .vsix — le Marketplace n'a pas de mode privé. Elle est attachée à chaque release du dépôt :

gh release download --repo Ajustor/speedster-js --pattern '*.vsix'
code --install-extension speedster-js-*.vsix

Pour un autre éditeur, le serveur de langage s'utilise directement en LSP : speedster-language-server --stdio. Voir Outillage.

2. tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    "noEmit": true,
    "skipLibCheck": true,

    "types": ["@ajustor/speedster/spd", "bun"],
    "paths": { "$messages": ["./src/lang/index.ts"] }
  },
  "include": ["src/**/*.ts", "src/**/*.spd"]
}

Ce jeu d'options est exactement celui que le service de langage utilise par défaut, et c'est la raison de le recopier tel quel : un projet plus permissif que l'éditeur verrait du rouge sur du code qui compile, un projet plus strict verrait du vert sur du code qui ne compile pas.

types: ["bun"] suppose @types/bun en dépendance de développement — sans le paquet, tsc s'arrête sur TS2688.

La ligne types fait accepter import Carte from './Carte.spd' par tsc. Elle ne vérifie rien : le typage exact des propriétés vient de speedster-check, décrit dans Outillage.

La ligne paths est celle du catalogue de messages. Déclarez-la même sans internationalisation : c'est l'alias que le compilateur cherche par défaut, et l'ajouter après coup est l'oubli classique — t perd son type dans l'éditeur et le build échoue sur Could not resolve: "$messages". Voir Internationalisation.

3. Le build

// build.ts
import { speedster } from '@ajustor/speedster/plugin'

const built = await Bun.build({
  entrypoints: ['src/main.ts'],
  outdir: 'dist',
  target: 'browser',
  minify: true,
  define: { 'process.env.NODE_ENV': '"production"' },
  plugins: [speedster()],
})

if (!built.success) throw new AggregateError(built.logs, 'build impossible')

define n'est pas décoratif : c'est lui qui retire les vérifications de développement du runtime. Sans cette ligne, elles partent dans le bundle livré.

4. La page

<!doctype html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Mon application</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

5. Le point d'entrée

// src/main.ts
import { mount } from '@ajustor/speedster'
import App from './App.spd'

mount(App, { target: document.getElementById('app')! })

mount retourne une fonction de démontage, qui retire exactement ce que le composant occupait — le contenu préexistant de la cible est préservé.

6. Le premier composant

<!-- src/App.spd -->
<script lang="ts">
  let compteur = 0
</script>

<button type="button" on:click={() => compteur++}>
  Cliqué {compteur} fois
</button>

<style>
  button { font: inherit }
</style>
bun run build.ts

C'est tout. Aucune déclaration de réactivité : compteur est réaffecté, donc c'est un état ; le texte le lit, donc il se met à jour. La suite est dans Composants et Réactivité.


Développer

speedster init écrit un dev.ts qui construit à la demande, surveille src/ et recharge la page à chaque sauvegarde.

Ce n'est pas du remplacement à chaud, et la nuance compte. Le framework fournit l'enveloppe — un composant garde la même fonction par identifiant, et ses instances vivantes sont remontées en place — mais aucun transport : rien dans le runtime ni dans le plugin ne surveille de fichier ni ne prévient le navigateur. Le serveur du modèle apporte le maillon manquant sous sa forme honnête : un rechargement complet, donc l'état est perdu. Un vrai remplacement à chaud demanderait de servir chaque module séparément, pour que seul celui qui a changé soit réévalué — ce que le build en un seul paquet ne permet pas.

// dev.ts — un serveur de développement minimal
import { speedster } from '@ajustor/speedster-plugin'

Bun.serve({
  port: 3000,
  development: true,
  async fetch(request) {
    const url = new URL(request.url)
    if (url.pathname === '/main.js') {
      const built = await Bun.build({
        entrypoints: ['src/main.ts'],
        target: 'browser',
        plugins: [speedster()],
      })
      return new Response(built.outputs[0]!.stream(), {
        headers: { 'content-type': 'text/javascript' },
      })
    }
    return new Response(Bun.file('index.html'))
  },
})

Vérifier

bunx speedster check src                # types + accessibilité
bunx speedster check src --a11y error   # accessibilité bloquante

À lancer avant de livrer, et en intégration continue : le code de sortie vaut 1 s'il reste une erreur.

Exécuter un .spd directement

Pour un test ou un script, bunfig.toml permet d'importer un composant sans passer par un build :

[test]
preload = ["@ajustor/speedster-plugin/preload"]

Les pièges du début

Trois choses surprennent, et une seule fois :

  1. La réactivité se fonde sur l'affectation. taches.push(x) ne met rien à jour ; taches = [...taches, x] si. Le compilateur le signale au build — voir Réactivité.
  2. Un composant n'est pas un élément. on:click sur <Carte /> est refusé : transmettez un gestionnaire en propriété. Un composant peut n'avoir aucune racine, ou plusieurs.
  3. Les avertissements d'accessibilité sont réels. Ils ne cassent rien par défaut, mais chacun décrit un mur pour quelqu'un. <!-- a11y-ignore-next --> existe pour les cas que la règle ne pouvait pas savoir.