Repository navigation
Initial stealth release
Pre-releaseJsxCore is a TSX/JSX view engine for ASP.NET Core. Write your views as .tsx files, return them from controllers or minimal APIs, and they render in the browser, on the server, or both.
This brings the experience of writing server and client rendered React / Preact / JSX to the .NET ecosystem without any real compromises. No bundler. No Node.js process. No hand-written model interfaces.
We've got all the stuff you expect from React - hooks, components, et al, along with server side rendering running right from .NET. You can use packages from NPM in your views, it works with MinimalAPI, and generally you can use things that work on the modern web.
This is, suffice to say, extremely experimental (0.0.1!) but I'm looking to bring it to production grade stability very shortly. Everything works and is tested, we just have to see where the wrinkles are before pushing out a 1.0.0.
dotnet add package JsxCorebuilder.AddJsxCore();
app.UseJsxCore();
app.MapGet("/", () => Results.Extensions.Jsx("Home/Index", new { name = "World" }));// Views/Home/Index.tsx
export default function Index({ model }: { model: { name: string } }) {
return <h1>Hello {model.name}</h1>;
}That is the whole setup. The build installs the TypeScript compiler on first run, so there is no step to forget.
What it does
Views are components. The same component renders on the server for first paint and SEO, in the browser for interactivity, or both with hydration, chosen per response.
No bundler. TypeScript rewrites ./Card.tsx to ./Card.js on the way out, so the browser resolves the module graph itself. Compiled modules are served from a content-hashed path with a one-year immutable cache header.
No Node process, ever. TypeScript 7 ships as a native binary that JsxCore starts directly, and server rendering runs JavaScript in-process. Nothing on your machine or your server runs Node.
Real .NET interop. Server-rendered views call .NET objects synchronously, in the request's own thread. No bridge, no serialisation, no async boundary to explain.
export default function Dashboard() {
return <p>{dotnet.Inventory.getSummary().total} in stock</p>;
}Types generated from your C#. View models are described once, in .NET. Declarations are generated during the build, from the assembly the build just produced, so a fresh clone type-checks without the application ever having been started.
import type { MyApp } from "@jsxcore/generated";
export default function Index({ model }: ViewProps<MyApp.Models.IndexModel>) { ... }Drops into MVC. Registers as an IViewEngine, so return View() finds Index.tsx through the normal view location rules. A view JsxCore cannot find falls through to Razor, so you can migrate one page at a time.
npm packages work. Install a package and import it. It resolves on the server and is served to the browser through a generated import map, with no bundler and no configuration. exports maps, subpaths, scoped packages and CommonJS interop are all handled.
Hot reload. Change a view, see it in the browser without losing state.
Preact, if you want it. One line switches the runtime, and React-targeted packages resolve through preact/compat. The built-in runtime is the default and is small enough to read in a sitting.
Packages without npm or node installed
JsxCore restores npm packages itself, by talking to the registry directly. It resolves versions, verifies integrity, unpacks into node_modules, and writes a package-lock.json that real npm ci accepts.
The .NET SDK is the only prerequisite. A machine with no Node and no npm builds a JsxCore project.
The same client ships as a dotnet tool:
dotnet tool install -g JsxCore.Npm
dotnet npm add marked --version ^12It is shaped after dotnet package add — add, remove, list, restore, ci, init, with --version, --dev and --project — and accepts npm's own spellings (install, i, ls, rm) where they are unambiguous. The executable is dotnet-npm, so your real npm is untouched.
Resolution is checked against npm rather than against a reading of the specification: version ranges are compared against npm's own semver package, and the trees produced for eslint, webpack and jest are compared against trees npm generates at the time of the test.
Already using npm? Keep using it. package.json, package-lock.json and node_modules are ordinary files in their ordinary formats. npm install works exactly as before and the build reads what it installed without touching the lock file npm wrote. <JsxCoreUseNpm>true</JsxCoreUseNpm> hands restoring back to npm entirely.
Requirements
- .NET 8, 9 or 10
- Nothing else. A published application needs only the .NET runtime
Built and tested on Linux, Windows and macOS.
Known limitations
- Node built-ins. Server rendering runs in an embedded JavaScript engine, not Node, so a package requiring
fs,pathorcryptofails with a message naming what it wanted. Browser-oriented and pure-JavaScript packages are fine. - Native modules. Anything with a
.nodebinary is out for the same reason. - Browser globals during server rendering. A package touching
windowat import time cannot be server-rendered; import it from a client-rendered view or guard the call. - npm lifecycle scripts are not run by the native client, because they are Node programs. Packages that compile something during installation need npm.
.npmrcbeyond the registry URL. Scoped registries, auth tokens and proxies are not read. A private registry needing authentication needs npm.- pnpm keeps transitive packages in
node_modules/.pnpm, so point JsxCore at it with<JsxCoreAdditionalSearchPaths>$(MSBuildProjectDirectory)/node_modules/.pnpm</JsxCoreAdditionalSearchPaths>. npm and Yarn need no configuration; Yarn's Plug'n'Play mode is unsupported.
Packages
| Package | What it is |
|---|---|
JsxCore |
The view engine. Add this to your web project |
JsxCore.Npm |
The npm client as a dotnet tool. Optional |
MIT licensed.