Synthèse automatique
Guide de création et de maintenance d'applications xzor, un framework de composants TypeScript inspiré de Blazor, compilé avec Bun et TypeScript 7.
Points forts
- Décrit précisément l'anatomie d'un fichier .xzor, les directives et la syntaxe de template ;
- Fournit les commandes d'initialisation, de développement et de build ;
- Met en garde contre les confusions avec Blazor en listant les API inexistantes ;
- Rappelle que les dossiers générés .xzor/ et dist/ ne doivent pas être modifiés.
Limites
- Contenu très spécifique à xzor, sans utilité en dehors de ce framework ;
- Suppose un environnement avec Bun et TypeScript 7 ;
- Le tableau de syntaxe du template est interrompu dans le contenu fourni, donc certains détails restent absents.
À utiliser lorsqu'on écrit, modifie ou explique des composants .xzor, des pages, des layouts ou le build d'une application xzor.
À ne pas utiliser pour des projets Blazor, React ou Vue, ni comme référence générale sur les composants TypeScript.
Analyse de sécurité
SûrThe SKILL.md is pure reference documentation (syntax tables, lifecycle, routing, DI, model tracking). It declares no allowed-tools, contains no shell pipelines, network calls, credential handling, obfuscation, or attempts to alter agent instructions. The only actions a reader would take are conventional package-install and build commands, which carry the ordinary supply-chain trust assumption of any framework guide but no elevated or malicious risk.
- •No executable, destructive, or exfiltration behavior: the file is documentation for a UI framework and contains only standard dev workflow commands (`bun add`, `bun x xzor init/build`, `bun run dev`).
- •Informational: following the guide installs and runs third-party npm packages (@source-crafter/xzor, xzor-runtime) and a build step that writes to `dist/`, `.xzor/` and `sw.js`; this is normal framework tooling but relies on trust in those packages.
Exemples
Create an xzor todo app with a TodoItem component, a Routes.xzor page, main.ts, and index.html. Run bun x xzor build and fix every reported error.Explain why <Router>, @Body, and StateHasChanged do not exist in xzor, and show the correct xzor patterns for routing, layout slots, and re-rendering.Add a TodoService class and inject it into a .xzor page using @inject, then update the component to load and display todos from that service.name: xzor description: Use when writing, changing or explaining .xzor components or an xzor app (Blazor-style components for TypeScript, packages @source-crafter/xzor and @source-crafter/xzor-runtime), including pages, routes, layouts, slots, cascading values, dependency injection, main.ts or the xzor build.
xzor
Overview
A .xzor file is one component: HTML with @ expressions plus a @code { } block of TypeScript class members. xzor build compiles each file to a TypeScript class, type-checks the project with TypeScript 7, and bundles main.ts into dist/main.js with Bun.
It looks like Blazor, but it is not Blazor. Many Blazor names don't exist here (<Router>, @foreach, @Body, LayoutComponentBase, @using, NavLinkMatch, StateHasChanged, createApp). When unsure, use the forms below; never invent APIs.
Turning a request into files
- Pick components (one
.xzoreach, PascalCase file name = component name), pages (components with@page), layouts (components with@slot) and services (.tsclasses). - Write
main.ts(the entry:@Mount),Routes.xzor(only if there are pages),index.html, and checktsconfig.json. - Run
bun x xzor buildand fix every error it reports before answering. Errors point at.xzorlines.
Project setup
bun add -d @source-crafter/xzor typescript@^7
bun add @source-crafter/xzor-runtime
bun x xzor init # package.json scripts, tsconfig.json, .gitignore (adds only what is missing)
bun run dev # xzor dev: build, serve (SPA fallback), print the URL; saving a .xzor patches the open page keeping state, other changes reload
bun x xzor build # one-off production build into dist/
xzor init writes this tsconfig.json (or adds the missing parts to an existing one):
{
"compilerOptions": {
"target": "ES2022", "module": "ESNext", "moduleResolution": "bundler",
"lib": ["ES2022", "ESNext.Disposable", "DOM"],
"strict": true, "noEmit": true, "skipLibCheck": true,
"rootDirs": [".", ".xzor"]
},
"include": ["**/*.ts", ".xzor/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
No experimentalDecorators: xzor uses standard decorators.
index.html loads the bundle, not main.ts:
<div id="app"></div>
<script type="module" src="./dist/main.js"></script>
Generated .xzor/ and dist/ are build output: never edit them. Don't create routes.ts at the root, and never import ./main from a component.
Component anatomy
@import { type EventCallback } from "@source-crafter/xzor-runtime"
@import TodoItem from "./TodoItem.xzor"
@code {
title = "Todos"; // a field is a prop the parent can set
todos: Todo[] = [];
changed?: EventCallback<number>; // callback prop; calling it re-renders the parent
add(text: string) { // inside @code it is plain TypeScript: use this.
this.todos.push({ text, done: false });
this.changed?.(this.todos.length);
}
}
<h2>@title (@todos.length)</h2>
- Imports go at the top as
@import ...lines (any ES import), never inside@code. Directives (@page,@import,@inject,@inherits, ...) can come in any order. - In the template, write member names bare (
@title,@add(x)): xzor addsthis.itself. Inside@code, writethis.. #privatemembers also work bare in the template:@#secret.
Template syntax
| Need | Write |
|---|---|
| Print a value | @name, @user.name, @items[0], @(a + b) for anything else; an array literal needs no parentheses: @['a', 'b'] |
| Literal @ | @@ (emails like a@b.com are fine as is) |
| Condition | @if (x) { ... } else if (y) { ... } else { ... } |
| Loop | @for (const item of items) { <li @key="item.id">@item.name</li> } (any JS for head; there is no @foreach) |
| Switch | @switch (v) { case "a": <p>A</p> break; default: <p>?</p> } |
| Statements | @{ const total = sum(items); } then @total |
| No wrapper | <text>...</text> |
| Comment | @* ... *@ |
| Attribute | class="item @kind", disabled="@(!ok)" (false/null removes it) |
| Event | @onclick="save", @onclick="() => count++", <form @onsubmit="add" @onsubmit:preventDefault> (modifiers combine with the handler) |
| Two-way binding | <input @bind="text" />, @bind:event="oninput", checkbox @bind="todo.done", numbers stay numbers |
| Bind a component | <Stepper @bind-value="n" /> (child has value and valueChanged?: EventCallback<T>) |
| DOM/instance ref | @ref="input" with a field input?: HTMLInputElement; |
| Spread attributes | @attributes="attrs" |
| Inline SVG | <svg viewBox="0 0 24 24"><path d="@d" /></svg> (keep the <svg> and its children in one place, not split across an @if/@for) |
Props on a child: <Child title="text" count="n + 1" onSave="save" />. A callback prop (onSave?: EventCallback<T>) can also be set like a DOM event: @onsave="save". A prop typed string, a union of string literals, an imported alias of string literals (type Size = "s" | "m"), or given a string default (name: Kind = "a") takes text; any other type takes a TypeScript expression. Misspelled or mistyped props are build errors.
An event re-renders only the component that owns the handler, and that render reads everything again (its fields, props and injected services). If shared state changes in another component's handler, call this.stateHasChanged() on the components that show it, or pass the value down as a prop.
Styles and module code
@module {
export type Kind = "a" | "b"; // module scope: exportable, no `this`
export const label = (k: Kind) => k.toUpperCase();
}
@code { kind: Kind = "a"; pct = 40; }
<div class="bar">@label(kind)</div>
<style>
.bar { width: @pct%; }
@if (kind === "b") { .bar { color: red; } }
</style>
- One top-level
<style>per component, or a siblingX.css.xzor(not both). Rules are scoped to the component's markup and the content it passes to children;<style global>(or@globalfirst inX.css.xzor) opts out. @name/@(expr)only in property values (one CSS custom property each);@if (...) { rules } else { rules }toggles rules;@@is a literal@;@media,@keyframes, ... stay CSS.@module { }(one per file) is module-level TypeScript: constants, types, helpers, markup values; other files import its exports from the.xzor. It can't read component members.
Lifecycle
protected override onInitialized() / onInitializedAsync() once; onParametersSet() / onParametersSetAsync() on mount and when props change; then render; then onAfterRender(first: boolean). shouldRender() can skip a render, dispose() runs on removal. A resolved ...Async promise re-renders.
Slots (layouts and wrappers)
@* Card.xzor *@
<section class="card">
@slot:header { <h3>Untitled</h3> } @* named slot with fallback *@
@slot @* default slot (this is Blazor's @Body / ChildContent) *@
</section>
<Card>
@slot header { <em>Today</em> }
<p>Everything else fills the default slot.</p>
</Card>
Slots can pass data: @slot:item(todo) in the component, @slot item(t) { <b>@t.text</b> } in the parent.
Cascading values
Provider: @cascade:theme(currentTheme) { @slot }. Consumer, at any depth: @code { @Cascade theme!: string; } (or @Cascade("theme") mode!: string;). Pass a new object to notify; in-place mutation is not detected.
Pages and routing
@* pages/TodoPage.xzor *@
@page "/todos/:id"
@code { id = 0; } @* ":id" fills the prop, converted to its type *@
<h2>Todo @id</h2>
One Routes.xzor (never itself a @page) holds the router and the layouts:
@import MainLayout from "./layouts/MainLayout.xzor"
@ClientRoutes(route) {
@notFound { <p>Nothing at @route.path</p> }
@catch(error) { <p class="error">@(String(error))</p> }
@default(page) { <MainLayout>@page</MainLayout> }
}
@pageworks in any.xzorof the project (apages/folder is only a convention). The component's class name is its file name.- A layout is an ordinary component with
@slotwhere the page goes. - Typed links:
@import { href } from "./routes"(relative path; the table is generated), then<a href="@href.TodoPage({ id: 3 })">. A page without parameters needs no argument:href.About(). @import { NavLink } from "@source-crafter/xzor-runtime", then<NavLink href="/" match="exact">Home</NavLink>: it addsclass="active"andaria-current="page"on its route;match="prefix"is the default.- Animated page changes: call
viewTransitions(true)(from@source-crafter/xzor-runtime) inmain.ts; give an element the sameview-transition-nameon both pages to morph it. - Plain
<a href="/about">links navigate without reloading. - Tag routes with
@page "/admin" as adminand give them a layout with@admin(page) { <AdminLayout>@page</AdminLayout> }. - Served under a sub-path (GitHub Pages): add
<base href="/repo/">toindex.html; routes andhreffollow it.
Services (compile-time dependency injection)
// main.ts: the entry. @Mount says which component renders into which element.
import { type IApplication, Mount, Singleton, Scoped } from "@source-crafter/xzor-runtime";
import Routes from "./Routes.xzor";
import { TodoService } from "./services/todos";
@Mount(Routes, "app") // <div id="app">
@Singleton(TodoService) // or @Singleton<ITodos>(TodoService), @Scoped(...), @Transient(...)
export class Application implements IApplication {}
@import { type TodoService } from "./services/todos" @* a type import is enough: main.ts creates it *@
@inject TodoService todos @* "Type name"; then use @todos / this.todos *@
- Constructor parameters of services are resolved by type (or by key name for
@Singleton("key", factory)). Classes are plain; no decorators on them. - Group reusable registrations in a
@ServiceProvider()class and add it with@Import(Provider)onApplication. - Never call
mount()yourself and don't give an@injectcomponent aconstructor. - Missing, ambiguous or cyclic services are build errors.
Models (state that tracks its changes)
Use a model for data that is edited and then saved: a form, a row of a table, anything where you need "what changed", "undo" or "is it valid". For plain UI state, keep using @code fields.
// models/worker.ts
import { Model, Persist, Validate, minValue, required } from "@source-crafter/xzor-runtime/model";
export class Worker extends Model {
@Validate(required) name = "";
@Validate(minValue(0)) hours = 0;
@Persist get payment() { return this.hours * 20; } // sent with the fields
}
@import { EntitySet } from "@source-crafter/xzor-runtime/model"
@import { Worker } from "../models/worker"
@code {
team = EntitySet.track(Worker, [{ name: "Ada", hours: 8 }]);
}
@for (const w of team.view) {
<input type="number" @bind="w.hours" @bind:event="oninput" /> @w.payment
<small>@(w.errors.hours?.[0])</small>
<button @onclick="() => team.delete(w)">Delete</button>
}
<button @onclick="() => team.revertChanges()" disabled="@(!team.hasChanges)">Revert</button>
<button @onclick="() => team.confirmChanges()" disabled="@(team.hasErrors)">Save</button>
- Write plain fields. The build turns them into tracked ones, and warns on stderr when a class is not tracked: fix every warning.
- Extend
Model, or another model class (its fields continue after the base's). Build one withnew Worker(data)(state Unchanged) ornew Worker()(Detached; the first edit makes it Added). If you write a constructor, callthis.assign(data)in it. - A component that reads a model while it renders draws again when that value changes, even if the change comes from a service or a timer. Do not call
stateHasChanged()for models. hasState(State.Modified | ...)(State.Detached | Unchanged | Added | Deleted | Modified, from.../model; there is nostateproperty),markAdded()/markModified()/markUnchanged()/markDeleted(),changes(what to send to the server,nullif nothing; the state is sent as$$, and$$in constructor data sets it),revertChanges(),confirmChanges(serverValues?),hasErrors,errors(messages per field, filled after the first edit),hasValidChanges.- Decorators:
@Validate(...),@Persist(a getter sent with the fields),@Ignore(kept out ofchanges),@Untracked(a plain field). Validators:required,email,phone,maxLength,minLength,requiredIdValue,nonZero,minValue,maxValue. EntitySet.track(Type, items)is a list of models:delete(item),view(without deleted),changes,hasChanges,hasErrors,revertChanges(),confirmChanges(serverItems?). Change it withpush,splice, ...;set[i] = xis not noticed. Read it throughvieworfor...of(notlength, an index orset.map) so the reader follows pushes and deletes.- Needs
"ESNext.Decorators"in the tsconfiglib. Not supported: plain object graphs with cycles, one set held by two models.
Fine components (the default)
Every component is compiled with no render pass: the DOM is built once and each dynamic part updates alone when a field it reads changes. Write @render on the first line only to get the virtual-DOM render pass instead.
- Its
@codefields are tracked like a model's:count++in a handler updates the page. Never callstateHasChanged()for that; call it only after changing something untracked (a plain array'spush, an object's property). - Put list data in an
EntitySet(or reassign a plain array) and loop with@for (const x of list.view): rows are kept by identity. - Totals and other derived values are plain expressions (
@team.view.reduce((s, w) => s + w.hours, 0)); no extra fields or setters. - The tsconfig
paths(when used) must map@source-crafter/xzor-runtime/modelas well as@source-crafter/xzor-runtime.
Hot reload and PWA
xzor devhot reload: saving a.xzorpatches the changed components in the open page and keeps their state; other changes reload. A#privatemember, a new import or a changed base class reload too, and arrow-function fields keep old code until a reload.- To make the app installable and offline, add
manifest.webmanifestnext toindex.html(name,short_name,start_url: "./",scope: "./",display, a 192x192 and a 512x512 icon, both files must exist) and<link rel="manifest" href="manifest.webmanifest">inindex.html. No decorator or code is needed. xzor buildthen writessw.jsbesideindex.htmland registers it (never inxzor dev). Don't edit it: it is generated and gitignored, but it must be deployed with the app. Offline loads fall back to the cached app; a new deploy shows up on the second load (the first fetches it in the background). Fix anyxzor: manifest.webmanifest: ...warning.
Common mistakes
| Wrong (Blazor or guess) | Right |
|---|---|
| createApp(App).mount(...), app.services.add... | @Mount(App, "app") + @Singleton(...) on Application in main.ts |
| <Router>, <RouteView>, <Found> | @ClientRoutes(route) { @default(page) { ... } } in Routes.xzor |
| @inherits LayoutComponentBase, @Body | a plain component with @slot |
| @foreach (var x in xs) | @for (const x of xs) |
| imports inside @code, @using | @import ... at the top |
| NavLinkMatch.All | match="exact" |
| <script src="main.ts"> | <script type="module" src="./dist/main.js"> |
| StateHasChanged() | this.stateHasChanged() |
| TypeScript 5, experimentalDecorators | TypeScript 7, standard decorators |
| @("<") to print < | < (HTML character references are decoded, like in Razor) |
Du fichier au premier essai
Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.
Consulter la source et ses fichiers annexesCe téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.
1. Télécharger le fichier
Télécharger SKILL.mdDepuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.
Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.
mkdir -p '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/xzor-components-blazor-style-typescript-mveraolu/download' --output '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md.part' && mv '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md.part' '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md'2. Vérifier les prérequis
Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.
Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.
Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.
3. Charger la skill
Après vérification, copiez le dossier complet dans le projet avec l’une des commandes ci-dessous, puis ouvrez une nouvelle session de l’outil. La commande s’arrête si un dossier du même nom existe déjà : comparez les fichiers avant de le remplacer.
Claude Code
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.claude/skills/xzor'Cursor
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.cursor/skills/xzor'Codex
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.agents/skills/xzor'OpenSkills
OpenSkills doit déjà être installé. La procédure locale install/read est contrôlée avec OpenSkills 1.5.0, qui exige Node.js ≥ 20.6.0. Aucun slug du catalogue n’est résolu automatiquement.
Installation officielle d’OpenSkillsopenskills install './.skill-downloads/xzor-components-blazor-style-typescript-mveraolu'openskills read 'xzor-components-blazor-style-typescript-mveraolu'La lecture doit afficher le contenu du fichier installé et son dossier de base. Elle ne lance ni la skill ni ses dépendances.
Pour un agent qui lit AGENTS.md, synchronisez ensuite l’index. Cette commande modifie AGENTS.md : relisez le diff avant de l’utiliser.
openskills syncMode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.
4. Faire un premier essai
Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :
Lis .skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.
Bonnes pratiques React Hook Form
Developpement
Maîtrisez React Hook Form : évitez les anti-patterns, gérez les abonnements, et traitez correctement reset, dirty state et entrées contrôlées.
Migration Emotion vers Mantine
Developpement
Migrer les styled-components Emotion vers des composants Mantine avec style props et modules CSS. Pour convertir .styled.tsx ou supprimer les imports @emotion.
Détecteur de dérive de périmètre
Developpement
Analyse les diffs git par rapport à une intention pour détecter la dérive de périmètre, fichiers non liés et PR trop larges, puis suggère de garder, diviser ou justifier.