Skip to content

Releases: davidwhitney/JsxCore

1.0.4

Choose a tag to compare

@davidwhitney davidwhitney released this 08 Oct 11:02
9529c21

What's Changed

  • Share one module resolver safely across concurrent renders: the first renders after a start could corrupt the shared NodeModuleResolver's manifest cache, after which every render failed with "Operations that change non-concurrent collections must have exclusive access". The cache is now concurrent and the runtime dependencies are published once (9529c21).

Full Changelog: 1.0.3...1.0.4

1.0.3

Choose a tag to compare

@davidwhitney davidwhitney released this 21 Sep 19:06

What's Changed

New Contributors

Full Changelog: 1.0.2...1.0.3

1.0.2

Choose a tag to compare

@davidwhitney davidwhitney released this 16 Aug 13:29

What's Changed

  • Adopt Jint 4.16.0: fewer per-render allocations, much cheaper engine construction by @lahma in #6

Full Changelog: 1.0.1...1.0.2

1.0.1

Choose a tag to compare

@davidwhitney davidwhitney released this 07 Aug 01:42

Behaviour change

ServerRendering.Timeout now bounds a whole render rather than each entry into the JavaScript
engine. One render enters the engine several times, so a view could previously consume several
multiples of the configured budget. A view that only fitted inside the old per-entry budgets can
now time out; ServerRendering.Timeout is the knob.

HttpContext.RequestAborted now stops running JavaScript, rather than only cancelling the wait for
a pooled engine.

Faster server rendering

Thanks to @lahma (#5) for the whole of this work, and for the benchmarks behind it.

  • Modules are parsed once per build and shared by every pooled engine, so growing the pool costs no
    extra parsing. The npm CommonJS to ESM transform runs once per build as well.
  • The host shims are parsed once per process, and the per-render ({}) and JSON.parse(...)
    evaluations are gone.
  • The model and context are serialised once per response instead of twice.
  • Server markup is returned as the string it already is, rather than being JSON-escaped inside the
    engine and unescaped on the way out.
  • An engine returning to the pool has its global surface restored, so anything a view left on
    globalThis no longer persists into a later request.
  • Jint updated to 4.15.3.

Measured on a warm engine: 2 to 10% faster depending on the view, and a pool-growth burst allocates
around half what it did. A cold start is 17 to 20% slower, because the first engine pays for the
static analysis that every later engine reuses; in production that is once per process.

Fixes

  • The first dotnet publish of a clean checkout omitted node_modules from the output, so
    container images and CI builds produced applications that returned 500 from any view importing a
    package (#3, reported by @jloor). The analysis that locates node_modules ran before the restore
    that creates it, and its answers were not re-read afterwards.
  • ServerRendering.Timeout could not be turned off. TimeSpan.Zero, Timeout.InfiniteTimeSpan and
    TimeSpan.MaxValue each armed a deadline at or before the render's own start, so every render
    failed immediately. Non-positive now means no budget, as it did before the whole-render deadline.

Documentation

Reported by @jloor (#4), from a full Razor to JsxCore port:

  • Troubleshooting entries for a model value rendering as nothing, which is what a casing mistake
    looks like from the outside, and for a form POST returning 400.
  • A "Razor request-state helpers" section in the .NET interop page, covering antiforgery and the
    validation helpers.

1.0.0 - Release

Choose a tag to compare

@davidwhitney davidwhitney released this 04 Aug 22:14
d128b52

JsxCore 1.0.0

Write your ASP.NET Core views in TSX. Return them from a controller. Never install Node.

JsxCore is a real IViewEngine for ASP.NET Core that renders JSX and TSX. Preact ships inside
the package; React is one project-file property away. Server-render for first paint, hydrate in
the browser, or both, chosen per response. Your view models are C#, and their TypeScript types
are generated from them, so the two can't drift.

The .NET SDK is the only prerequisite.

Sixty seconds

dotnet add package JsxCore
builder.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>;
}
dotnet run.

That's the whole setup.

What's in it

No Node. No npm. Not even a little. JsxCore talks to the npm registry itself, in C#. A clean checkout builds on a machine that has never had Node installed, it fetches the native TypeScript compiler and the esbuild binary and gets on with it.

No bundler. TypeScript rewrites ./Card.tsx to ./Card.js and the browser resolves the module graph natively. Nothing to configure, no import map to hand-write.

Views are components. The same component renders on the server for SEO and first paint, mounts in the browser for interactivity, or both. You pick per response.

Real .NET interop. Server rendering runs in-process, so a view calls your C# services synchronously, during render. No fetch, no API, no bridge.

Types generated from your C#. Describe a model once, in .NET. The .tsx gets the declarations.

Drops into MVC. return View() finds Index.tsx. Minimal APIs get Results.Extensions.Jsx.

Release builds precompile your views and publish them, so the server needs no compiler and no npm. Minified with esbuild, served Brotli or gzip.

Also shipping

JsxCore.Npm, a dotnet npm tool. Add, restore and list npm packages with nothing installed but the .NET SDK. It resolves versions, verifies integrity, and writes a package-lock.json that real npm accepts.

dotnet tool install --global JsxCore.Npm
dotnet npm add marked

Supported

Built and tested on:

  • .NET 8, 9 and 10. Linux, macOS and Windows.

Better TSConfig generation

Pre-release

Choose a tag to compare

@davidwhitney davidwhitney released this 04 Aug 00:55
94e7d2c

Less flickering on typedefs between builds

0.0.5 - Possible first release candidate

Choose a tag to compare

@davidwhitney davidwhitney released this 03 Aug 15:35
9b6be87

This is relatively bug free, and relatively feature complete - but I'm still polishing the user facing APIs so this might change or break.

Initial stealth release

Pre-release

Choose a tag to compare

@davidwhitney davidwhitney released this 29 Jul 16:57
e8dad74

JsxCore 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 JsxCore
builder.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 ^12

It 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, path or crypto fails with a message naming what it wanted. Browser-oriented and pure-JavaScript packages are fine.
  • Native modules. Anything with a .node binary is out for the same reason.
  • Browser globals during server rendering. A package touching window at 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.
  • .npmrc beyond 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.