Composants xzor (style Blazor en TypeScript)

VérifiéSûr

Écrire, modifier et expliquer des composants .xzor : pages, routes, layouts, slots, valeurs en cascade, injection de dépendances et build via Bun.

Spar Skills Guide Bot
DeveloppementIntermédiaire
02/11/2026
Claude CodeCursorCodex
#xzor#typescript#component-framework#build-tool#blazor-style

Recommandé pour

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.
Quand l'utiliser

À utiliser lorsqu'on écrit, modifie ou explique des composants .xzor, des pages, des layouts ou le build d'une application xzor.

Quand l'éviter

À 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ûr
Score qualité89/100

The 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.

Points d'attention
  • •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.
Comment cette analyse est faite

Exemples

Build an xzor todo app
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 xzor vs Blazor
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 service with dependency injection
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

  1. Pick components (one .xzor each, PascalCase file name = component name), pages (components with @page), layouts (components with @slot) and services (.ts classes).
  2. Write main.ts (the entry: @Mount), Routes.xzor (only if there are pages), index.html, and check tsconfig.json.
  3. Run bun x xzor build and fix every error it reports before answering. Errors point at .xzor lines.

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 adds this. itself. Inside @code, write this..
  • #private members 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 sibling X.css.xzor (not both). Rules are scoped to the component's markup and the content it passes to children; <style global> (or @global first in X.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> }
}
  • @page works in any .xzor of the project (a pages/ folder is only a convention). The component's class name is its file name.
  • A layout is an ordinary component with @slot where 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 adds class="active" and aria-current="page" on its route; match="prefix" is the default.
  • Animated page changes: call viewTransitions(true) (from @source-crafter/xzor-runtime) in main.ts; give an element the same view-transition-name on both pages to morph it.
  • Plain <a href="/about"> links navigate without reloading.
  • Tag routes with @page "/admin" as admin and give them a layout with @admin(page) { <AdminLayout>@page</AdminLayout> }.
  • Served under a sub-path (GitHub Pages): add <base href="/repo/"> to index.html; routes and href follow 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) on Application.
  • Never call mount() yourself and don't give an @inject component a constructor.
  • 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 with new Worker(data) (state Unchanged) or new Worker() (Detached; the first edit makes it Added). If you write a constructor, call this.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 no state property), markAdded()/markModified()/markUnchanged()/markDeleted(), changes (what to send to the server, null if 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 of changes), @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 with push, splice, ...; set[i] = x is not noticed. Read it through view or for...of (not length, an index or set.map) so the reader follows pushes and deletes.
  • Needs "ESNext.Decorators" in the tsconfig lib. 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 @code fields are tracked like a model's: count++ in a handler updates the page. Never call stateHasChanged() for that; call it only after changing something untracked (a plain array's push, 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/model as well as @source-crafter/xzor-runtime.

Hot reload and PWA

  • xzor dev hot reload: saving a .xzor patches the changed components in the open page and keeps their state; other changes reload. A #private member, 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.webmanifest next to index.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"> in index.html. No decorator or code is needed.
  • xzor build then writes sw.js beside index.html and registers it (never in xzor 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 any xzor: 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 < | &lt; (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 annexes

Ce 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.md

Depuis 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’OpenSkills
openskills 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 sync

Mode 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.

Skills similaires