How to Run
Sharpen is a build-time generator, not a bundler plugin. It reads a .csx
file once and writes a real .tsx/.ts file next
to it — nothing downstream ever touches .csx.
npm install -g @mail4hafij/sharpen
sharpen Foo.csx -o Foo.tsx
sharpen --dir ./src
License
Sharpen is free to use for 30 days from the first time it runs on a given machine. After that, a license is required to keep using it.
To request a license, email mail4hafij@yahoo.com.
Editor Support
A VS Code extension adds syntax highlighting for .csx files.
code --install-extension mail4hafij.sharpen-csx
Or search "Sharpen" in VS Code's Extensions panel. Syntax highlighting only for now — no IntelliSense or go-to-definition yet.
Documentation
Every real feature, grouped by what it touches: React-specific syntax, then general-purpose TypeScript syntax that applies equally on the backend.
React Syntax
Component — [Component], JSX Render()
[Component]
public class Greeting
{
[Prop] private string name;
public JSX Render() => <p>Hello, {name}!</p>;
}
interface GreetingProps {
name: string;
}
export function Greeting({ name }: GreetingProps) {
return (<p>Hello, {name}!</p>);
}
State & Props — [State], [Prop]
[State] private int count = 0;
private void Increment() => count++;
const [count, setCount] = useState<number>(0);
const increment = () => setCount(count + 1);
Children — [Prop] private JSX children;
[Prop] private JSX children;
public JSX Render() => <div className="panel">{children}</div>;
JSX maps to ReactNode. Lowercase
children is already React's own reserved prop name — React itself,
not Sharpen, populates it whenever a parent nests content inside
<YourComponent>...</YourComponent>.
Conditionals & loops in markup — @if / @foreach
@if (numbers.Count == 0)
{
<p>No numbers yet.</p>
}
else
{
<ul>
@foreach (var n in numbers)
{
<li>{n}</li>
}
</ul>
}
{numbers.length === 0 ? <p>No numbers yet.</p> : <ul>{numbers.map(n => <li>{n}</li>)}</ul>}
Fragments — <>...</>
Works both as a component's whole Render() return value, and inside an @if/@foreach branch — the only way to return more than one sibling element from a branch.
public JSX Render() =>
<>
<p>Seed: {seed}</p>
<button onClick={Increment}>+1</button>
</>;
return (<><p>Seed: {seed}</p><button onClick={increment}>+1</button></>);
Effect — [Effect], [Effect(Layout = true)]
[Effect(Deps = nameof(value))]
private void TrackUpdate() => updateCount++;
useEffect(() => {
setUpdateCount(updateCount + 1);
}, [value]);
More than one dependency works too — [Effect(Deps = nameof(a), nameof(b))]
compiles to [a, b], not just the first one.
[Effect(Layout = true)] compiles to useLayoutEffect
instead of useEffect — same body/deps handling either way, coexists with
Deps on the same attribute. An async effect
body runs inside an IIFE automatically, since useEffect's own callback
can't be async.
Refs — [Ref], [ForwardedRef]
[Ref] private HTMLInputElement inputRef;
[Effect]
private void FocusOnMount() => inputRef.current?.focus();
public JSX Render() => <input ref={inputRef} />;
[Component]
[Memoized]
public class ForwardRefInput
{
[Prop] private string placeholder;
[ForwardedRef] private HTMLInputElement inputRef;
public JSX Render() => <input placeholder={placeholder} ref={inputRef} />;
}
export const ForwardRefInput = memo(forwardRef<HTMLInputElement, ForwardRefInputProps>(function ForwardRefInput({ placeholder }: ForwardRefInputProps, inputRef) {
return (<input placeholder={placeholder} ref={inputRef}/>);
}));
Memoization — [Memo], [Callback], [Memoized]
| Attribute | Applies to | Compiles to |
|---|---|---|
[Memo(Deps = nameof(x))] | a method returning a value | useMemo — calling it in source becomes the bare value, not a call |
[Callback(Deps = nameof(x))] | a method | useCallback — stays a real callable in source, unlike [Memo] |
[Memoized] | a [Component] class, stacked next to [Component] | wraps the whole component in React's memo() |
[Memo(Deps = nameof(numbers))]
private int Total() => numbers.Sum();
public JSX Render() => <p>Total: {Total()}</p>;
const total = useMemo(() => numbers.reduce((a, b) => a + b, 0), [numbers]);
return (<p>Total: {total}</p>);
Deps supports more than one dependency here too, the same way [Effect] does — Deps = nameof(a), nameof(b) keeps both, not just the first.
Context & Provider — [Context], [UseContext], dotted JSX tags
[Context]
public class ThemeContext
{
public string Theme { get; set; } = "light";
}
[UseContext] private ThemeContext theme;
public JSX Render() => <button className={theme.Theme}>Click me</button>;
Context.Providerpublic record ThemeValue(string Theme);
[Prop] private JSX children;
public JSX Render() =>
<ThemeContext.Provider value={new ThemeValue("dark")}>
{children}
</ThemeContext.Provider>;
A dotted tag's root gets the same automatic cross-file import a [UseContext] field already gets — no explicit import needed even when the context lives in a different file.
Store — [Store], [Observable], [Inject]
[Store]
public class CounterStore
{
[Observable] public int Count { get; set; } = 0;
public void Increment() => Count++;
}
export const useCounterStore = create<{
count: number;
increment: () => void;
}>(set => ({
count: 0,
increment: () => set(s => ({ count: s.count + 1 })),
}));
Bare [Inject] subscribes to the whole store (every mutation re-renders).
[Inject(Select = nameof(Store.Member))] subscribes to just one field
(useStore(s => s.member)) — use this for anything performance-sensitive.
A store action can be a real async API call — full statement grammar (locals, if/try,
await), not just a bare mutation. See the Restaurant App below for a full example.
Custom hooks — [Hook]
public record CounterHook(int Count, Action Increment, Action Reset);
[Hook]
public static CounterHook UseCounter(int initial = 0)
{
[State] int count = initial;
void increment() => count++;
void reset() => count = initial;
return new CounterHook(count, increment, reset);
}
export function useCounter(initial: number = 0): CounterHook {
const [count, setCount] = useState<number>(initial);
const increment = () => setCount(count + 1);
const reset = () => setCount(initial);
return { count, increment, reset };
}
TypeScript Syntax
No React here — plain classes and functions, equally usable in the Node.js backend.
Types
Every C# type this compiler understands, and what it becomes.
| C# | TypeScript |
|---|---|
int, double, float, long | number |
string | string |
bool | boolean |
void | void |
T? | T | null |
List<T> | T[] |
Action | () => void |
Action<T1, T2, ...> | (arg0: T1, arg1: T2, ...) => void |
Task | Promise<void> |
Task<T> | Promise<T> |
dynamic | any |
JSX | ReactNode |
dynamic is for a shape deliberately left unmodeled — e.g. a raw database row from a driver, with no ORM in front of it. JSX is used for a [Prop] meant to hold renderable content, most commonly a component's children.
Imports — using
using { readFileSync } from "fs";
using path from "path";
using * as cors from "cors";
import { readFileSync } from "fs";
import path from "path";
import * as cors from "cors";
A relative-path using (e.g. using { Foo } from "./Foo";)
is recognized as your own code, so ClassName.Method() correctly camelCases
the member even called by class name. A real package import keeps its casing untouched.
Classes & functions
public class Rectangle
{
public double Width = 0;
public double Height = 0;
public double Area() => Width * Height;
}
export class Rectangle {
public width: number = 0;
public height: number = 0;
public area(): number {
return this.width * this.height;
}
}
A plain function works the same way: no attribute needed, just a real exported function with real parameters, arithmetic, and local variables.
Records & enums
public record Todo(int Id, string Text, bool Done);
private enum Status { Loading, Success, Error }
export interface Todo {
id: number;
text: string;
done: boolean;
}
type Status = "loading" | "success" | "error";
new Todo(1, "Buy milk", false) compiles to a plain object literal { id: 1, text: "Buy milk", done: false }; todo with { Done = true } compiles to { ...todo, done: true }. Both a record and an enum work equally well declared top-level (as here) or nested inside a class/component/store/context.
Control flow — if, for, foreach, ternary
int total = 0;
for (int i = 0; i < n; i++)
{
total = total + i;
}
return total > 10 ? "big" : "small";
let total: number = 0;
for (let i: number = 0; i < n; i++) {
total = total + i;
}
return total > 10 ? "big" : "small";
foreach (var word in words)
{
console.log(word);
}
for (const word of words) {
console.log(word);
}
foreach compiles to a real for...of, not .forEach(). while/do and a plain switch aren't supported yet — deferred until something concretely needs them.
Operators
| C# | TypeScript |
|---|---|
+ - * / | identical |
< > <= >= | identical |
== != | === !== (strict, not loose) |
&& || | identical |
?? | identical |
cond ? a : b | identical |
?? sits outermost of the three (loosest), then ||, then && — a simplification of real JS's stricter mixing rules, never produces a wrong-precedence result.
Lambdas
var doubled = numbers.Select(n => n * 2);
var total = doubled.Aggregate(0, (a, b) => a + b);
let doubled = numbers.map(n => n * 2);
let total = doubled.reduce((a, b) => a + b, 0);
A single bare parameter needs no parentheses (n => ...); more than one does ((a, b) => ...). Expression-bodied only — a block-bodied lambda (() => { ... }) isn't supported yet.
Optional chaining & null-forgiving
inputRef.current?.focus();
someValue!.doSomething();
Template literals — $"..."
var greeting = $"Hello, {name}! You have {count} item{(count == 1 ? "" : "s")}.";
let greeting = `Hello, ${name}! You have ${count} item${count === 1 ? "" : "s"}.`;
C#'s own interpolated-string syntax, not JS's backtick literal directly — stays consistent with "write real C#, get idiomatic TS out." Each {...} hole is parsed as a fully independent expression (arbitrary nesting works).
Arrays — spread, indexing, as
Categories = [.. Categories, created]; // -> [...categories, created]
var rows = result[0]; // tuple/array indexing
var info = result[0] as ResultSetHeader; // type assertion, compile-time only
LINQ (LINQ-to-Objects only)
Every method below operates on a plain in-memory array — the same as Array.prototype itself. There is no LINQ-to-SQL translation anywhere; the backend talks to the database via raw SQL strings regardless.
| LINQ | Compiles to |
|---|---|
Where(pred) | .filter(pred) |
Select(fn) | .map(fn) |
ToList() | (no-op — already an array) |
Sum() | .reduce((a,b)=>a+b,0) |
OrderBy(key) | .slice().sort(...) — non-mutating |
Any() / Any(pred) | .length > 0 / .some(pred) |
All(pred) | .every(pred) |
First() / FirstOrDefault() | [0] / .find(pred) |
Count() / Count(pred) | .length / .filter(pred).length |
Skip(n) / Take(n) | .slice(n) / .slice(0, n) |
Reverse() | .slice().reverse() — non-mutating |
Distinct() | [...new Set(arr)] (reference equality for objects) |
Min() / Max() | Math.min(...arr) / Math.max(...arr) |
Aggregate(seed, fn) | .reduce(fn, seed) |
Join(inner, ok, ik, result) | a nested .flatMap()/.filter()/.map() — one row per matching pair, like a SQL inner join |
GroupBy/ToDictionary/ToLookup aren't implemented — their result is a different shape entirely (a grouped sequence, a Map), not just another array.
Async, await, try/catch
[Effect(Deps = nameof(userId))]
private async void LoadUser()
{
try
{
await fetchUser(userId);
status = Status.Success;
}
catch
{
status = Status.Error;
}
}
A method can also return a real value asynchronously — Task/Task<T> is the real async return type (see Types above), not void:
public class Repo
{
public static async Task<int> CountRows(string sql)
{
var result = await Query(sql);
return result;
}
}
export class Repo {
public static async countRows(sql: string): Promise<number> {
let result = await Query(sql);
return result;
}
}
Error Handling
Every error csx reports is exactly one of three kinds — the kind itself tells
you what to do next. Every error includes the exact file, line, and column, plus a short code frame
pointing right at the problem.
| Kind | Meaning | What to do |
|---|---|---|
| Syntax error | You wrote something invalid — malformed grammar, or a real rule violation like a store action that never mutates state | Fix your .csx |
| Unsupported feature | Valid C#, but this compiler doesn't emit it yet | Not a mistake — restructure around the known gap |
| Internal compiler error | The compiler itself hit something it didn't expect | Not your fault — please report it |
Syntax errors — malformed grammar
Something the parser can't make sense of at all — a missing token, a bad tag, an unterminated string.
[Component]
public class Greeting
{
[Prop] private string name
public JSX Render() => <p>Hello, {name}!</p>;
}
Greeting.csx:5:5 - syntax error: Expected ";"
5 | public JSX Render() => <p>Hello, {name}!</p>;
^
[Component]
public class Greeting
{
[Prop] private string name;
public JSX Render() => <p>Hello, {name}!</div>;
}
Greeting.csx:5:50 - syntax error: Mismatched JSX closing tag: expected </p>, got </div>
5 | public JSX Render() => <p>Hello, {name}!</div>;
^
Semantic rule violations
Grammatically fine, but breaks a real rule the compiler enforces — reported as the same "syntax error"
kind, since either way the fix is in your .csx. The one rule checked today: an
expression-bodied [Store] action must actually mutate an
[Observable] property.
[Store]
public class CounterStore
{
[Observable] public int Count { get; set; } = 0;
public int Peek() => Count;
}
CounterStore.csx:5:5 - syntax error: Store method "Peek" must mutate an [Observable] property
5 | public int Peek() => Count;
^
Unsupported features
Real, valid C# — just not emitted yet. Not a mistake to fix, a gap to restructure around.
[Component]
public class Greeting
{
[Prop] private string name;
public JSX Render() { return <p>Hello, {name}!</p>; }
}
Greeting.csx:5:25 - unsupported feature: Render() must be expression-bodied (=> JSX); block bodies are not supported for Render yet
5 | public JSX Render() { return <p>Hello, {name}!</p>; }
^
Multiple errors at once
A mistake in one declaration doesn't hide a mistake in another — every independent problem in a file (and
every broken file in a --dir run) is collected and reported together in one
pass, not discovered one fix-and-recompile cycle at a time.
[Component]
public class Dashboard
{
[Prop] private string name;
[Memo(Deps = nameof(name))]
private string Shout()
{
return name;
}
public JSX Render() => <p>{name}</p>;
}
[Store]
public class CounterStore
{
[Observable] public int Count { get; set; } = 0;
public int Peek() => Count;
}
Dashboard.csx:6:5 - unsupported feature: Block-bodied [Memo] method "Shout" isn't supported yet
6 | [Memo(Deps = nameof(name))]
^
Dashboard.csx:19:5 - syntax error: Store method "Peek" must mutate an [Observable] property
19 | public int Peek() => Count;
^
Internal compiler errors
Looks the same way — file:line:col plus a code frame when a position is available — but means the mistake
is on the compiler's side, not yours. There's nothing to fix in your .csx for
one of these; it exists so a raw crash never leaks out disguised as your own bug, and it's always worth
reporting.
The Restaurant App
A small real app — categories and menu items — with both halves built and verified end to end: a
React frontend calling a real Node.js/Express/MySQL API, both written entirely in
.csx and compiled by Sharpen.
The architecture, briefly:
- One
[Store]holds both the domain data (categories/items) and shared UI state (which modal is open, draft field values) — that's what lets List/Add/Edit live in separate files without passing a full object across them as a prop. - Each
Listowns its ownAdd/Editas children, not as siblings — delete is handled byListitself. - Every component selects only the store slices it actually reads (
[Inject(Select = ...)]), not the whole store. - Store actions are real async API calls —
await fetch(...), real response handling, adopting the server's response instead of inventing state locally.
Frontend
app/src/
├── App.csx
├── stores/
│ └── RestaurantStore.csx categories, items, and all shared UI state
└── pages/
├── Category/
│ ├── CategoryList.csx renders CategoryAdd + CategoryEdit as children
│ ├── CategoryAdd.csx
│ └── CategoryEdit.csx
└── Item/
├── ItemList.csx renders ItemAdd + ItemEdit as children
├── ItemAdd.csx
└── ItemEdit.csx
public async Task SaveNewCategory()
{
var body = JSON.stringify(new NewCategoryBody(CategoryDraftName, "/images/category-placeholder.svg"));
var response = await fetch(ApiUrl("/categories"), JsonRequest("POST", body));
var created = await response.json();
Categories = [.. Categories, created];
SelectedCategoryId = created.Id;
ShowAddCategoryForm = false;
}
[Component]
public class CategoryList
{
[Inject(Select = nameof(RestaurantStore.Categories))] private List<Category> categories;
[Inject(Select = nameof(RestaurantStore.SelectedCategoryId))] private int selectedCategoryId;
[Inject(Select = nameof(RestaurantStore.SelectCategory))] private Action<int> selectCategory;
[Inject(Select = nameof(RestaurantStore.DeleteCategory))] private Action<int> deleteCategory;
public JSX Render() =>
<div>
<div className="category-grid">
@foreach (var c in categories)
{
<div key={c.Id} className={c.Id == selectedCategoryId ? "category-card selected" : "category-card"}>
<p onClick={() => selectCategory(c.Id)}>{c.Name}</p>
<button onClick={() => deleteCategory(c.Id)}>Delete</button>
</div>
}
</div>
<CategoryAdd />
<CategoryEdit />
</div>;
}
[Component]
public class CategoryAdd
{
[Inject(Select = nameof(RestaurantStore.ShowAddCategoryForm))] private bool showAddCategoryForm;
[Inject(Select = nameof(RestaurantStore.CategoryDraftName))] private string categoryDraftName;
[Inject(Select = nameof(RestaurantStore.SetCategoryDraftName))] private Action<string> setCategoryDraftName;
[Inject(Select = nameof(RestaurantStore.SaveNewCategory))] private Action saveNewCategory;
public JSX Render() =>
<div>
@if (showAddCategoryForm)
{
<div className="modal">
<input value={categoryDraftName} onChange={e => setCategoryDraftName(e.target.value)} />
<button onClick={saveNewCategory}>Save</button>
</div>
}
</div>;
}
Backend
A real Node.js/Express backend, MySQL-backed (schema in api/schema.sql).
8 endpoints, verified with real HTTP requests against the real database.
| Resource | List | Add | Edit | Delete |
|---|---|---|---|---|
| Category | GET /categories |
POST /categories |
PUT /categories/:id |
DELETE /categories/:id |
| Item | GET /items |
POST /items |
PUT /items/:id |
DELETE /items/:id |
api/src/
├── main.csx entry point - see the Main() convention below
├── db/
│ └── Database.csx static factory: Database.Connect() -> Pool
├── categories/
│ ├── Category.csx record Category(Id, Name, ImageUrl)
│ └── CategoryRepository.csx static methods: List/Add/Edit/Delete against MySQL
├── items/
│ ├── MenuItem.csx
│ └── ItemRepository.csx
└── routes/
├── CategoryRoutes.csx static methods: registers the 4 category endpoints
└── ItemRoutes.csx static methods: registers the 4 item endpoints
public static async Task<int> Edit(Pool pool, int id, string name, string imageUrl)
{
var result = await pool.query("UPDATE categories SET name = ?, image_url = ? WHERE id = ?", [name, imageUrl, id]);
var info = result[0] as ResultSetHeader;
return info.affectedRows;
}
public static void Register(Express app, Pool pool)
{
app.get("/categories", (req, res) => List(pool, req, res));
app.put("/categories/:id", (req, res) => Edit(pool, req, res));
}
public static async Task Edit(Pool pool, Request req, Response res)
{
var id = Number(req.params.Id);
var affected = await CategoryRepository.Edit(pool, id, req.body.Name, req.body.ImageUrl);
if (affected == 0)
{
res.status(404).end();
return;
}
res.json(new Category(id, req.body.Name, req.body.ImageUrl));
}
using { Database } from "./db/Database";
using { CategoryRoutes } from "./routes/CategoryRoutes";
using { ItemRoutes } from "./routes/ItemRoutes";
public async void Main()
{
var pool = Database.Connect();
Express app = express();
app.use(express.json());
CategoryRoutes.Register(app, pool);
ItemRoutes.Register(app, pool);
app.listen(4000, () => console.log("API listening on http://localhost:4000"));
}
A plain function literally named Main is auto-invoked at the bottom of
the file — the one way to get a top-level statement in a grammar where everything else is
a declaration. No constructors exist yet, so every backend class here is a
static-method namespace, not an instance.