speedster-js Guide

Outillage

Dix paquets, dont un qui les rassemble. Un seul, @ajustor/speedster-runtime, part dans le navigateur ; les autres tournent au build ou dans l'éditeur.

Paquet Rôle
@ajustor/speedster Tout en un : le runtime à plat, l'outillage sous /build
@ajustor/speedster-runtime Réactivité et primitives DOM — la seule dépendance d'exécution
@ajustor/speedster-plugin Plugin de build Bun, rechargement à chaud
@ajustor/speedster-check Vérificateur de types pour les .spd
@ajustor/speedster-report Rapport de build : poids, problèmes, accessibilité
@ajustor/speedster-language-service Survol, complétion, navigation pour l'éditeur
@ajustor/speedster-language-server Le même service, exposé en LSP
@ajustor/speedster-router Routeur de navigateur et fournisseurs typés
@ajustor/speedster-cli Génération d'éléments et amorçage de projet
@ajustor/speedster-i18n Compilation des catalogues de messages
@ajustor/speedster-compiler Le compilateur lui-même

Installer @ajustor/speedster suffit : il porte les autres, et bun lie aussi leurs binaires. Le poids livré au navigateur ne change pas — bun run size mesure les deux chemins d'import et les trouve identiques à l'octet près.

L'extension VS Code se distribue à part, en .vsix : voir L'éditeur.

Une seule lib, deux surfaces

import { signal, mount } from '@ajustor/speedster'            // navigateur
import { runtime, transitions } from '@ajustor/speedster'      // idem, groupé
import { plugin, check, i18n } from '@ajustor/speedster/build'  // build
import { renderToString } from '@ajustor/speedster/server'      // rendu serveur

La séparation entre . et /build est le seul point de ce paquet qui mérite une explication. Le projet tient sur une promesse : rien d'autre que le runtime ne part dans le navigateur. Réunir les deux surfaces dans un même module rendrait cette promesse dépendante du tree-shaking de l'empaqueteur — il faudrait qu'il élague typescript et node:fs à chaque build, et le jour où il n'y parvient pas, l'échec est soit un bundle multiplié par cent, soit une erreur de résolution de node:fs en cible navigateur. Deux points d'entrée le garantissent par construction.

Le rendu serveur a aussi le sien : le poser à la racine ajoutait 1,56 ko minifiés pour du code qui ne tourne jamais dans un navigateur, et ses types Component et Snippet désignent autre chose que ceux du navigateur — empiler du texte au lieu d'insérer du DOM.

Chaque paquet garde par ailleurs son propre sous-chemin : @ajustor/speedster/plugin, /compiler, /check, /report, /i18n, /language-service, /generators, /runtime, /transitions.


Générer un élément

bunx speedster new component Carte
bunx speedster new store panier
bunx speedster new page Accueil
bunx speedster init mon-app

Une seule commande à retenir : speedster passe aussi la main aux deux autres, en sous-commandes.

bunx speedster check src --a11y error
bunx speedster i18n src/lang --out src/lang --default fr

Les binaires speedster-check et speedster-i18n restent installés et fonctionnent tels quels — l'ombrelle ne les remplace pas, elle les rassemble.

Les types

Type Produit Dossier par défaut
route Une route +page.spd, par son chemin src/routes
layout Un gabarit +layout.spd src/routes
service Une classe @injectable src/app
component Un composant .spd src/components
store Un module d'état partagé src/magasin
action Une action use: src/actions
transition Une transition transition: src/transitions
page Composant, rendu serveur, reprise navigateur src

Ce qui sort n'est pas un fichier vide. Un composant arrive avec un titre relié par aria-labelledby, un aria-expanded sur le bouton qui le pilote et une propriété annotée — trois choses que speedster-check réclamerait de toute façon. Un magasin mute par update et non par set sur une lecture. Une action déclare update et destroy, dont l'oubli est l'erreur la plus coûteuse ici : sans destroy, chaque montage laisse un écouteur derrière lui, et rien ne le signale.

Le test du paquet vérifie exactement cela : tout ce que les générateurs produisent passe speedster-check avec l'accessibilité en erreur.

Options

Option Effet
--dir <chemin> Dossier de destination, à la place du défaut du type
--no-props État local plutôt que propriétés exportées
--fragments Contenu transmis par le parent (Snippet)
--no-style Sans bloc <style>
--load Ajoute le +page.ts voisin (route seulement)
--force Écrase les fichiers existants
--dry-run Montre ce qui serait écrit, sans rien écrire

Un fichier existant n'est jamais écrasé sans --force. On relance volontiers un générateur pour créer le frère du précédent, et perdre le composant qu'on vient d'écrire pour une faute de frappe dans le nom serait une mauvaise surprise.

speedster init

bunx speedster init mon-app
cd mon-app && bun install && bun run dev

Ce qui s'ouvre n'est pas une page blanche : une application habillée — en-tête avec le logo et la navigation, corps centré, pied de page — un message de bienvenue, un compteur qui prouve la réactivité, un bouton qui change de langue, et trois cartes qui disent où aller ensuite. Le premier bun run dev montre donc quelque chose dont on peut partir, plutôt qu'un titre nu sur fond blanc.

Le projet fonctionne d'emblée, internationalisation comprise : deux langues, un catalogue déjà compilé, et un serveur de développement qui le régénère à chaque traduction enregistrée. La boucle entière est là :

Script Ce qu'il fait
bun run dev Construit à la demande, surveille src/, recharge la page
bun run check Types et accessibilité, en une étape
bun run typecheck tsc --noEmit seul, sans le vérificateur .spd
bun run build Bundle minifié dans dist/, page comprise
bun run preview Sert dist/ comme le ferait un hébergeur statique
bun run i18n Compile les catalogues de src/lang/ — dev et build le font seuls
bun run routes Régénère la table des routes — idem

Une seule dépendance d'exécution, @ajustor/speedster. @types/bun et typescript en développement — le premier n'est pas un détail : le tsconfig.json déclare types: ["bun"], et sans le paquet tsc s'arrête sur TS2688.

Aucun fichier d'outillage à la racine. Les deux derniers scripts sont les commandes speedster i18n et speedster routes ; build.ts et dev.ts, qui en ont besoin avant d'empaqueter, appellent les deux mêmes fonctions de bibliothèque — huit lignes, écrites en clair là où on les modifie :

const messages = await i18n.compileProject({ source: 'src/lang', out: 'src/lang', defaultLocale: 'fr' })
const table = await routes.writeRoutes({ dir: 'src/routes', outDir: 'src' })

Ni les commandes ni ces appels ne dupliquent quoi que ce soit : compileProject et writeRoutes sont dans les paquets, et tout le monde passe par eux. C'est ce qui garantit qu'un catalogue régénéré à la sauvegarde est exactement celui du build.

L'internationalisation, sans étape oubliée

Le catalogue est compilé dès l'amorce, et non laissé à un premier bun run i18n : sinon $messages ne résout pas, et un bun run typecheck juste après la création échouerait sur un projet que l'on vient de créer.

Ensuite, dev.ts et build.ts le recompilent eux-mêmes — le second avant d'empaqueter, le premier au démarrage puis à chaque src/lang/*.json enregistré, avant de faire recharger le navigateur : l'ordre inverse le ferait reconstruire sur le catalogue précédent. Le serveur ignore ses propres écritures pour ne pas boucler.

Deux langues sont amorcées, pas une. Une seule ne prouverait rien : c'est la seconde qui révèle les clés oubliées et qui donne un sens à locale.set. Le message d'exemple porte un pluriel ICU, la forme qu'on écrit mal la première fois si aucun exemple ne l'a montrée.

Les fichiers compilés sont volontairement hors du .gitignore : ils doivent exister pour qu'un clone fraîchement installé passe le typage.

Le style, et où il vit

Le style d'un composant ne sort pas de ce composant : c'est la règle, et l'amorce s'y tient — chaque .spd porte le sien. Restent trois choses qui n'appartiennent à aucun composant en particulier : la remise à zéro du navigateur, les variables de thème que tous lisent, et ce qui vise html, body ou :root, que la portée ne peut pas atteindre. Elles sont dans public/styles.css, liée par index.html.

Un fichier de public/, et non un import du bundle : la feuille est ainsi appliquée avant que le premier script ne soit évalué. Passée par l'empaqueteur, la page s'afficherait nue le temps du téléchargement de main.js. Le serveur de développement sert public/ à la racine comme le fait dist/ en production, et recharge la page quand un de ses fichiers change — sans quoi une feuille de style modifiée demanderait un redémarrage, et une couleur essayée coûterait dix secondes.

Le thème tient en une dizaine de variables — marque, fonds, bordures, texte, rayon, largeur du corps — déclinées en clair et en sombre par une seule requête média. En changer une suffit à retourner toute l'application.

Le reste

build copie index.html et public/ dans dist/ : un dossier qu'on ne peut pas ouvrir n'est pas un build fini. preview le sert sur le port 4173, minifié et sans rechargement, ce qui est le seul moyen de voir ce qui sera réellement livré.

Pourquoi ce tsconfig.json

Ses options ne sont pas un assortiment de bonnes pratiques : c'est exactement le jeu que le service de langage utilise par défaut. 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. Les deux sont pénibles, et le second est le pire. Aligner supprime la question.

C'est aussi le seul fichier où un réglage manquant ne se manifeste pas par une erreur claire :

  • sans types: ["@ajustor/speedster/spd"], import App from './App.spd' est refusé par tsc alors que le build le résout très bien ;
  • sans paths: { "$messages": … }, t perd son type dans l'éditeur et le build échoue sur Could not resolve: "$messages" dès la première traduction ;
  • sans verbatimModuleSyntax, le compilateur et l'éditeur cessent de s'accorder sur ce qui est un type.

En code

import { component, writeBlueprint } from '@ajustor/speedster-cli'

const plan = component('Carte', { dir: 'src', props: true, fragments: false, style: true })
await writeBlueprint(plan, { root: process.cwd(), force: false, dryRun: false })

Un générateur ne touche pas au disque : il décrit des fichiers. L'écriture, la création des dossiers et le refus d'écraser vivent ailleurs — c'est ce qui rend les gabarits testables sans système de fichiers, et --dry-run gratuit.


Le plugin de build

import { speedster } from '@ajustor/speedster-plugin'

await Bun.build({
  entrypoints: ['src/main.ts'],
  outdir: 'dist',
  target: 'browser',
  minify: true,
  define: { 'process.env.NODE_ENV': '"production"' },
  plugins: [speedster()],
})
Option Défaut Effet
hmr actif hors production Émet le rechargement à chaud
mode 'client' 'server' produit des modules de rendu vers une chaîne
hydratable true Marqueurs d'hydratation, en mode serveur
css 'file' Le style sort dans un .css minifié ; 'inject' le remet dans le module
accessibility 'warning' 'error' rend les manquements bloquants, 'off' les tait
i18nModule '$messages' Module fournissant le catalogue
root process.cwd() Racine, pour des identifiants de module stables
filter /\.spd$/ Fichiers pris en charge
onWarning affichage annoté Reçoit les avertissements du compilateur

Le <link> de la feuille est à poser. Le style d'un composant sort du JavaScript : l'empaqueteur écrit un .css minifié, dont le nom porte une empreinte. Une page qui ne le déclare pas est servie sans style, et rien ne le signale — les build.ts et dev.ts qu'amorce speedster init le posent donc depuis la sortie du build, et s'arrêtent si aucune feuille n'a été produite. Voir Où va le CSS.

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é — et le script de mesure du dépôt échoue exprès si du code de rechargement à chaud survit à un build.

Un autre empaqueteur

transform est exporté séparément : il prend une source et rend du TypeScript.

import { transform } from '@ajustor/speedster-plugin'

const { code, diagnostics } = transform(source, chemin, { hmr: false })

C'est tout ce qu'il faut pour brancher Vite, esbuild ou Rollup.

Exécuter un .spd directement

# bunfig.toml
[test]
preload = ["@ajustor/speedster-plugin/preload"]

Un composant s'importe alors sans build, en test comme en exécution.


Le rechargement à chaud

Ce qui existe est l'enveloppe, pas le transport. La distinction est la seule chose importante de cette section.

L'enveloppe : hors production, le compilateur émet pour chaque composant un appel qui le met en cache par identifiant. Un module réévalué retrouve donc la même fonction — un parent qui la référence reste valide et n'a pas besoin d'être réexécuté — et les instances vivantes sont remontées à leur place. L'état interne du composant remonté est perdu, celui du reste de l'application est conservé.

Le transport, lui, n'existe pas : ni dans le runtime, ni dans le plugin, rien ne surveille les fichiers, rien ne prévient le navigateur, rien ne réévalue le module modifié. L'enveloppe attend un appelant que le framework ne fournit pas.

Le dev.ts de speedster init apporte donc le maillon manquant sous sa forme honnête : il surveille src/, prévient le navigateur par websocket, et celui-ci recharge la page. L'état est perdu — c'est un rechargement, pas un remplacement.

Un vrai remplacement à chaud demanderait de servir chaque module séparément, pour que seul celui qui a changé soit réévalué et que le registre de l'enveloppe survive. Le build en un seul paquet ne le permet pas : réévaluer le bundle recrée aussi le registre, et il n'y a plus rien à retrouver.

En production, rien de tout cela n'est produit — le module de développement n'apparaît même pas dans le graphe de dépendances. Il n'y a donc rien à élaguer.


Le vérificateur de types

bunx speedster-check src
bunx speedster-check src/Page.spd src/lib.ts
bunx speedster-check src --project tsconfig.json
bunx speedster-check src --a11y error

Un dossier est parcouru récursivement. Le code de sortie vaut 1 s'il reste une erreur : c'est une étape d'intégration continue telle quelle.

Ce qu'il apporte par rapport à tsc :

<Carte titre={42} />
Page.spd:6:14 — erreur TS2322
Type 'number' is not assignable to type 'string'.
6 | <Carte titre={42} />
  |              ^^^^

Le compilateur exporte une interface Props pour chaque composant ; le vérificateur présente chaque .spd à TypeScript comme un module, et ramène chaque erreur à sa position dans le .spd — pas dans du code que personne n'a écrit.

Le tsconfig.json du projet est lu s'il existe : les alias de chemins — celui du catalogue de messages, par exemple — y sont résolus comme au build.

Ses options remplacent celles par défaut, qui sont strictes. Un projet dont le tsconfig.json n'active pas strict sera donc vérifié sans : c'est votre configuration qui fait foi, et --project permet d'en désigner une autre.

Avec tsc

La déclaration de repli fait accepter import Carte from './Carte.spd' par tsc sans rien de plus :

{ "compilerOptions": { "types": ["@ajustor/speedster-plugin/spd"] } }

Elle ne vérifie rien — les propriétés y sont un dictionnaire ouvert. Les deux cohabitent sans risque : speedster-check résout chaque .spd vers son module généré, et cette résolution l'emporte.


Le service de langage

Pour un éditeur, ou n'importe quel outil qui a besoin de répondre pendant qu'on tape.

import { createLanguageService } from '@ajustor/speedster-language-service'

const service = createLanguageService({ currentDirectory: 'src' })

service.openDocument('src/Page.spd', texte)
service.updateDocument('src/Page.spd', texteModifie)

service.getDiagnostics('src/Page.spd')
service.getQuickInfoAtPosition('src/Page.spd', 128)
service.getCompletionsAtPosition('src/Page.spd', 128)
service.getDefinitionAtPosition('src/Page.spd', 128)

Il réunit deux moitiés :

  • TypeScript, sur le module généré. La carte de positions est lue dans les deux sens : à l'aller pour porter le curseur du .spd vers le module, au retour pour ramener la réponse à sa place. Survoler {compteur} affiche let compteur: number, et demander la définition mène à la ligne du <script>.
  • Le balisage, dont TypeScript ne voit rien : {#, on:, bind:, role=". Le vocabulaire ARIA proposé est la même table que celle qui refuse un aria-lable au build — l'éditeur et le compilateur ne peuvent pas être en désaccord.

Il répond aussi sur un composant momentanément invalide : la dernière compilation réussie sert la complétion, et les diagnostics de type sont supprimés dans cet état — ils tomberaient à des positions périmées, sur un texte qu'on est en train de corriger.

Les chemins rendus sont en séparateurs /, quelle que soit la plateforme.


L'éditeur

Le service ci-dessus est une bibliothèque : il faut encore le brancher. Deux paquets s'en chargent, et la séparation n'est pas gratuite.

@ajustor/speedster-language-server l'expose en LSP. Il ne contient aucune intelligence — il traduit : des décalages vers des couples ligne/colonne, des chemins vers des URI, le vocabulaire du service vers celui du protocole.

speedster-language-server --stdio

Ce binaire suffit à Neovim, Zed ou Helix. La chaîne complète — service, compilateur, TypeScript — n'utilise aucune API propre à Bun, ce qui lui permet aussi de tourner sous Node.

L'extension VS Code, elle, embarque ce serveur et le fait exécuter par le Node de VS Code : rien n'est demandé à la machine, pas même Bun.

Elle embarque aussi les lib.*.d.ts de TypeScript, et indique leur dossier au serveur. Ce n'est pas de la prudence : ts.getDefaultLibFilePath part de sys.getExecutingFilePath(), qui suppose un node_modules/typescript/lib voisin — une supposition qui tombe dans un bundle, où ce chemin est soit figé sur la machine de build, soit absent. Le second cas fait disparaître toute la bibliothèque standard, et l'erreur n'en dit rien : Record, String, HTMLElement deviennent simplement « introuvables », comme si le composant était en faute.

Vient de
Coloration, indentation, repli, icône La grammaire de l'extension
Complétion, survol, définition, diagnostics Le serveur

La distinction compte à l'usage : la coloration tient même si le serveur tombe, et speedster.enable: false retire les réponses sans retirer les couleurs.

La coloration

La grammaire connaît ce que le compilateur connaît, et rien de plus : {#if}, {#each}, {#key}, {#snippet}, {:else}, {@html}, {@render}, {@const}, les huit préfixes de directive (on:, bind:, class:, style:, use:, transition:, in:, out:) et leurs modificateurs |. Un nom de balise capitalisé est traité comme un composant — la même règle que le compilateur.

Le <script> est coloré comme du TypeScript, le <style> comme du CSS, et une interpolation aussi : {items.map((item) => ({ id: item.id }))} garde ses accolades imbriquées, ce qui demande de les compter — TextMate ne sait pas le faire seul.

Les messages traduits dans l'éditeur

t n'a pas de déclaration dans le composant : le compilateur ajoute son import, depuis $messages par défaut. L'éditeur le résout donc par les paths du tsconfig.json — c'est ce qui fait que survoler t.panier.titre affiche (property) titre: string et que la complétion propose les clés du catalogue.

Si le projet passe un autre i18nModule au plugin, dites-le aussi à l'éditeur, sinon il résoudrait un module que le build n'utilise pas :

{ "speedster.i18nModule": "~/messages" }

Installer

L'extension n'est pas publiée sur le Marketplace, qui n'a pas de mode privé. Le .vsix est attaché à chaque release du dépôt :

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

Depuis les sources :

bun run --cwd packages/vscode vsix

Les messages

bunx speedster i18n src/lang --out src/lang --default fr
bunx speedster i18n src/lang --out src/lang --runtime @ajustor/speedster

--runtime nomme le module d'où le catalogue importe le signal de langue. speedster i18n le déduit du package.json : un projet qui n'installe que la façade reçoit @ajustor/speedster, un projet qui déclare @ajustor/speedster-runtime reçoit celui-ci. Sans cette déduction, l'import écrit dans le catalogue résoudrait par hasard, à travers un node_modules aplati, jusqu'au jour où il ne résout plus.

Voir Internationalisation.


La table des routes

bunx speedster routes
bunx speedster routes --dir src/routes --out src
bunx speedster routes --eager          # tout dans un seul morceau

Un dossier contenant un +page.spd est une route ; la commande écrit src/routes.generated.ts. Le module du routeur se déduit comme celui du runtime ci-dessus.

Chaque page est découpée : la table l'importe dynamiquement, donc l'empaqueteur en fait un fichier séparé que le navigateur ne télécharge qu'au moment d'y aller. Mesuré sur six pages : 6,96 ko compressés au premier chargement contre 8,10 ko, et 6,33 contre 10,33 dès qu'une page traîne une dépendance lourde. --eager remet tout dans le morceau d'entrée, ce qui n'a de sens que pour une application d'une ou deux pages minuscules. Voir Routeur.

Le serveur de développement régénère la table à chaque route ajoutée ou retirée : cette commande sert quand il ne tourne pas — après un git pull, ou en intégration continue. Le code de sortie vaut 1 si aucune route n'a été trouvée, parce qu'une table vide compile, s'importe, et n'affiche rien.

Voir Routeur.


Le compilateur, directement

Utile pour un outil sur mesure : un formateur, un analyseur, une génération de documentation.

import { compile, parse, analyze } from '@ajustor/speedster-compiler'

const { code, map, diagnostics, hasErrors } = compile(source, {
  filename: 'Page.spd',
  tolerant: true,       // rend les diagnostics au lieu de lever
  mode: 'client',
  accessibility: 'warning',
})

const arbre = parse(source)                 // pour un formateur
const analyse = analyze(arbre)              // pour un analyseur

Les trois étapes sont utilisables séparément. map est la correspondance entre le module généré et le .spd : originalRangeOf ramène une position produite à sa place, generatedPositionOf fait le trajet inverse.


Le rapport de build

bun run report          # construit, puis écrit rapport.md
bunx speedster report   # sur un dist déjà construit
bunx speedster report --budget 60 --json

Poids du premier chargement face à un budget, problèmes groupés par règle, accessibilité à part, couverture des traductions, table des routes. Le code de sortie vaut 1 s'il reste une erreur ou si le budget est dépassé.

Voir Rapport de build.


Mesurer le poids, à la main

Le dépôt du framework contient bun run size, qui compile de vrais composants avec le vrai plugin et mesure le morceau d'entrée. Le même principe s'applique à votre projet :

const built = await Bun.build({ /* … */ })
const code = await built.outputs[0]!.text()
const octets = new TextEncoder().encode(code)
console.log(Bun.gzipSync(octets, { level: 9 }).byteLength)

Ce qui compte n'est pas la taille du paquet mais celle de ce qui survit à l'élagage. Le rapport ci-dessus fait la même mesure, sur le dossier livré tout entier.


En intégration continue

- run: bun install
- run: bun run i18n                       # catalogues à jour
- run: bunx speedster-check src --a11y error
- run: bun test
- run: bun run build.ts
- run: bunx speedster report --budget 60  # poids et accessibilité, archivables

speedster-check couvre les types et l'accessibilité : une seule étape suffit pour les deux. La dernière ligne est facultative — elle échoue sur un budget dépassé et laisse un rapport.md à attacher à la construction.


Publier les paquets

Les onze paquets partent sur npm, publics, sous le scope @ajustor. La pipeline s'authentifie avec le secret NPM_TOKEN du dépôt : un jeton npm granulaire, en lecture-écriture sur le scope, qui contourne la 2FA.

Une release se pose, se commite, puis se tague :

bun run release:version 0.1.0
git commit -am "chore: version 0.1.0"
git tag v0.1.0 && git push origin v0.1.0

Ce sont les manifestes qui portent la version ; le tag ne fait que la confirmer. bun pm pack produit donc localement le bon numéro, et un lecteur du dépôt sait où il en est — ce qui n'était pas le cas d'un dépôt figé à 0.0.0.

Le prix de ce choix est qu'un tag peut diverger de ce que les manifestes annoncent. La pipeline vérifie au lieu de réécrire : release:version --check compare le tag aux dix manifestes et fait échouer la release sur une divergence, au lieu de publier en silence quelque chose que le dépôt ne revendique pas.

.github/workflows/release.yml enchaîne ensuite : types, tests, vérification du tag, publication dans l'ordre des dépendances, puis empaquetage de l'extension VS Code et attachement du .vsix à la release.

L'extension suit la même version que les paquets sans être publiée sur le registry : c'est elle qui nomme le fichier attaché, et un .vsix en 0.0.0 face à des paquets en 0.3.0 ne se retrouve plus.

Répéter un tag est sans danger, des deux côtés : --tolerate-republish fait qu'une release interrompue en cours de route reprend là où elle s'était arrêtée au lieu de buter sur les paquets déjà en ligne, et le .vsix remplace le précédent au lieu d'échouer sur une release déjà créée.

Pour dérouler la chaîne sans rien pousser :

bun run release:publish --dry-run

Une contrainte à connaître

Les dépendances internes sont déclarées en workspace:*, et bun publish les remplace par la version que le lockfile associe au paquet — pas par celle du manifeste. Un bump de version périme donc le lockfile ; release:version le rafraîchit lui-même. Changer les versions à la main sans relancer bun install publierait des paquets qui dépendent de versions inexistantes.

Consommer les paquets

Le registre est public : ni jeton ni .npmrc.

bun add @ajustor/speedster