# NiteFx > NiteFx is TypeScript and React for ASP.NET Core with a Vite-style dev loop and no Node.js toolchain. It compiles client code on request with esbuild, keeps component state across hot updates, installs npm packages with its own C# client, and ships hashed, code-split bundles from `dotnet publish`. It needs the .NET SDK and nothing else. ## Notes for AI coding agents NiteFx looks like Vite but is not Vite, and habits from Vite and Node projects produce code that does not work. In a NiteFx project: - There is no `package.json`, `node`, `npm`, `npx` or `vite.config.*`. Do not create them. Packages are listed in `nite.json` and locked in `nite.lock.json`. - Add packages with `nite add ` (`nite add -D ` for dev-only ones such as types), remove them with `nite remove `, and update them with `nite update`. Never `npm install`. - Run the app with `nite watch` (or `dotnet watch` / `dotnet run`); there is no separate frontend server or dev proxy. `dotnet publish -c Release` runs `nite build` and needs the `nite` tool (`dotnet tool install --global Nite.Cli`). - Client code lives in the client root, `Client/` by default. `Program.cs` calls `AddNite()`, `UseNite()` and, for a single-page app, `MapNiteSpa()`; Razor apps use `` tag helpers. The API is on the same origin, so there is no CORS to configure. - Build-time settings come from `import.meta.env.NITE_PUBLIC_*`; `envPrefix` in `nite.json` sets other prefixes (projects imported from Vite keep `VITE_`). Per-request values from the server come from `NiteOptions.ClientConfig` and `import { config } from "nite:config"`. - Not supported: `import.meta.glob`, Vite import suffixes (`?raw`, `?url`, `?worker`), Vite, Rollup and Babel plugins, and server-side rendering. NiteFx strips types without checking them; run `tsc --noEmit` for type errors. - The project is NiteFx (Nite Framework). Its NuGet packages (`Nite`, `Nite.Cli`, `Nite.Templates`), its C# namespace (`Nite`), the `nite` command and its files (`nite.json`, `/_nite`) keep the short name. # Introduction NiteFx runs TypeScript and React inside ASP.NET Core the way Vite runs them in Node: compiled on request, with hot updates that keep component state, and a production build that ships hashed, code-split bundles. It needs the .NET SDK and nothing else. *NiteFx* is short for **Nite Framework**, and *Nite* is pronounced like **night**, not *neat*. (Vite, whose dev loop it borrows, is French for "quick" and is pronounced *veet*.) The NuGet packages (`Nite`, `Nite.Cli`, `Nite.Templates`), the C# namespace and the `nite` command keep the short name. ## The problem Putting a modern frontend on an ASP.NET Core app usually means running two toolchains side by side: - a Node.js install, `package.json`, a lockfile and a bundler (Vite, webpack) for the client; - the .NET SDK, a project file and `dotnet watch` for the server; - a dev proxy or CORS settings so the two servers can talk, and a build step in CI that stitches the outputs together. Each half is good at what it does. The seams between them are where the time goes: two terminals, two hot-reload systems that know nothing about each other, a proxy configuration that drifts from production, and a Node version to keep current on every machine and build agent. ## What NiteFx does instead NiteFx moves the frontend dev server into your ASP.NET Core app. One project, one process, one `dotnet watch`: - **Edit a React component** and it updates in place, with its state intact (React Fast Refresh). - **Edit a C# endpoint** and hot reload applies it in the running app; the page gets a `nite:server-update` event and re-fetches, without reloading. - **Run `dotnet publish`** and the client is built into minified, content-hashed bundles in `wwwroot/_nite`, served precompressed by `MapStaticAssets`. There is no `node`, `npm`, `package.json` or Vite anywhere. Compilation is done by [esbuild](https://esbuild.github.io), the same Go binary Vite uses for dependency pre-bundling, which NiteFx downloads and drives itself. Packages come from the npm registry through a small npm client written in C#. ## Two ways to use it **A single-page app.** `Client/index.html` and React (or plain TypeScript), served by `app.MapNiteSpa()` next to your API. The app and the API share an origin, so there is no CORS and no proxy. See [Single-page apps](https://nitefx.dev/docs/guides/single-page-apps.md). **Islands in Razor pages.** Server-rendered Razor Pages or MVC views with interactive React components exactly where you need them: ```cshtml @* Server-rendered fallback until the island loads *@
    @foreach (var t in Model.Todos) {
  • @t.Title
  • }
``` See [Islands in Razor Pages](https://nitefx.dev/docs/guides/islands.md). ## How it compares | | NiteFx | Vite + ASP.NET Core | SPA templates (`SpaProxy`) | LibMan / CDN scripts | |---|---|---|---|---| | Node.js required | No | Yes | Yes | No | | Processes in development | One | Two | Two (proxied) | One | | TypeScript and JSX | Yes (esbuild) | Yes | Yes | No | | React Fast Refresh | Yes | Yes | Yes | No | | C# changes reach the page without a reload | Yes (`nite:server-update`) | No | No | No | | npm packages | Yes (C# client) | Yes | Yes | A curated subset | | Sass, Less, Tailwind, PostCSS, CSS modules | Yes | Yes | Yes | No | | Islands in Razor views | Built in | Manual | No | Manual | | Vite and Rollup plugins | No | Yes | Yes | No | | Framework-specific SSR (Next.js, Remix) | No | Via the framework | Via the framework | No | NiteFx is deliberately smaller than Vite. If your project depends on Vite plugins (Vue single-file components, Svelte, MDX), or on a meta-framework with its own server, keep using those tools. [Limits and non-goals](https://nitefx.dev/docs/limits.md) lists what NiteFx does not do. ## What it is good for - Internal tools, dashboards and line-of-business apps where the team writes C# and wants a React frontend without adopting the Node ecosystem's operations. - Razor sites that need a handful of interactive widgets without becoming a single-page app. - Teams that want one build, one deployment artifact and one dev command. - Moving an existing Vite or Create React App frontend next to its API, keeping the exact package versions it runs today ([Importing a project](https://nitefx.dev/docs/guides/importing.md)). ## Status NiteFx is at version **0.8**. The core loop (compilation, Fast Refresh, islands, packages, stylesheets, production builds) is complete and tested against real applications, including a [Mantine](https://nitefx.dev/docs/guides/mantine.md) app and a [Tailwind CSS 4](https://nitefx.dev/docs/guides/tailwind.md) app. The packages are [on NuGet](https://www.nuget.org/packages/Nite.Cli), so [installation](https://nitefx.dev/docs/getting-started.md#install-nitefx) is one `dotnet tool install`. ## Next steps - [Getting started](https://nitefx.dev/docs/getting-started.md): install the CLI and create your first app. - [Tutorial](https://nitefx.dev/docs/tutorial.md): build a small full-stack app and see each part of the dev loop. - [How NiteFx works](https://nitefx.dev/docs/concepts/architecture.md): the moving parts, for when you want to know what happens on save. # Getting started Install the CLI, create an app, and run it with `nite watch`. About five minutes, most of it the first package download. ## Requirements - **The .NET 10 SDK.** Check with `dotnet --version`. - **Linux, macOS or Windows** on x64 or Arm64. NiteFx downloads the matching esbuild (and, if you use Sass, Dart Sass) binary for your platform. - **Network access to the npm registry** the first time: NiteFx downloads esbuild and your packages, then caches them per user. After that, startup reuses the cache and the lockfile. A private registry or mirror works too; see [Private registries](https://nitefx.dev/docs/guides/private-registries.md). You do **not** need Node.js, npm, or any global JavaScript tooling. ## Install NiteFx NiteFx is three packages on nuget.org: **`Nite`**, the library your web project references; **`Nite.Cli`**, the `nite` command-line tool; and **`Nite.Templates`**, optional `dotnet new` templates. How release binaries are signed, and who approves it, is in the [Code signing policy](https://nitefx.dev/docs/code-signing.md). # [NuGet (recommended)](https://nitefx.dev/docs/getting-started.md#tab/nuget) ```bash dotnet tool install --global Nite.Cli nite version ``` New projects reference the `Nite` package from nuget.org with no further setup. To update later, run `nite update-self`. # [Reference a source checkout](https://nitefx.dev/docs/getting-started.md#tab/source) If you are working on NiteFx itself, point new projects at your checkout instead. The project then references `src/Nite` and `src/Nite.Cli` directly, so changes to NiteFx take effect on the next build. ```bash git clone https://github.com/nite-net/nite cd nite && dotnet build # Run the CLI from the checkout. dotnet run --project src/Nite.Cli -- init ../my-app --nite-source . ``` Inside such a project, run the CLI the same way (`dotnet run --project /src/Nite.Cli -- watch`), or install the tool from NuGet as in the other tab. --- ## Create an app `nite init` creates a project in a new folder (or the current one) and installs its packages. # [Single-page app](https://nitefx.dev/docs/getting-started.md#tab/spa) ```bash nite init my-app cd my-app ``` ```text my-app/ my-app.csproj Program.cs AddNite, UseNite, an /api/hello endpoint, MapNiteSpa nite.json packages and settings for the client nite.lock.json resolved versions (commit it) Client/ index.html the entry page main.tsx createRoot(...).render() App.tsx calls /api/hello and re-fetches on C# changes styles.css tsconfig.json node_modules/ installed packages (ignored by git) ``` # [Razor Pages with islands](https://nitefx.dev/docs/getting-started.md#tab/islands) ```bash nite init my-site --template islands cd my-site ``` ```text my-site/ my-site.csproj Program.cs Razor Pages, AddNite, UseNite nite.json Pages/ _ViewImports.cshtml @addTagHelper *, Nite Shared/_Layout.cshtml in Index.cshtml Client/ islands/Counter.tsx a React component used as an island wwwroot/site.css ``` --- > [!TIP] > Set your preferred template once with `nite config set init.template islands`. ### With dotnet new The same templates come as a `dotnet new` template pack, which also lists them in Visual Studio's and Rider's new-project dialogs: ```bash dotnet new install Nite.Templates dotnet new nite-spa -o my-app # Single-page app dotnet new nite-islands -o my-site # Razor Pages with islands ``` `nite init` and `dotnet new` write the same files. The difference: `nite init` installs the packages in `nite.json` straight away, while with `dotnet new` the first `nite watch` (or `nite install`) does. You still need the `nite` tool to run and publish the app. Update the templates with `dotnet new update`. ## Run it ```bash nite watch ``` `nite watch` runs `dotnet watch` with NiteFx handling the browser. The first start downloads esbuild and pre-bundles React (a few seconds); after that it starts in about as long as the app takes to build. `dotnet watch` opens your browser at the URL in `Properties/launchSettings.json`. Now try the loop: 1. **Edit `Client/App.tsx`.** Change the heading and save. The page updates in place in about 150 ms. Click the button a few times first: the count survives the edit. 2. **Edit `Program.cs`.** Change the message `/api/hello` returns and save. Hot reload applies it in the running app and the page re-fetches, still without a reload. 3. **Break something.** Delete a closing brace in `App.tsx`. An overlay shows the error with its location; fixing it clears the overlay. 4. **Add an endpoint.** Add a new `app.MapGet(...)` line to `Program.cs`. That needs a restart, so `nite watch` restarts the app and the page reloads once it is back (about 10 seconds). For the Razor template, run `nite watch --browser-refresh` instead, so `dotnet watch` reloads the page when a `.cshtml` file changes. See [Hot reload and nite watch](https://nitefx.dev/docs/guides/hot-reload.md). ## Add a package ```bash nite add react-router # a runtime dependency nite add -D @types/node # a dev dependency (types, tooling) ``` `nite add` updates `nite.json`, resolves versions into `nite.lock.json` and installs into `node_modules`. If the app is running, the dev server notices the change, pre-bundles the new package and reloads the page. See [Packages](https://nitefx.dev/docs/guides/packages.md). ## Publish ```bash dotnet publish -c Release ``` Release builds run `nite build`, which writes minified, content-hashed bundles to `wwwroot/_nite`; the publish output serves them precompressed through `MapStaticAssets`. Nothing in the published app needs Node, esbuild or `node_modules`. See [Production builds and deployment](https://nitefx.dev/docs/guides/deployment.md). ## Already have a Vite app? ```bash nite init my-app --from ../my-vite-app --dry-run # see the plan nite init my-app --from ../my-vite-app # do it; the source is not changed ``` See [Importing a project](https://nitefx.dev/docs/guides/importing.md). ## Working with an AI coding agent NiteFx is newer than the training data of most AI models, and it looks enough like Vite that an agent working from memory will reach for `npm install` and `vite.config.ts`. Two things help: - New projects include an `AGENTS.md`, which most coding agents (Codex, Cursor, GitHub Copilot, Claude Code and others) read on their own. It lists the commands, the project layout and the Vite habits that do not apply. Edit it as the project grows; it is yours. If you also keep a `CLAUDE.md`, Claude Code reads that instead, so put `@AGENTS.md` at its top to include both. - The docs are published for agents to read: [`/llms.txt`](https://nitefx.dev/llms.txt) is an index, [`/llms-full.txt`](https://nitefx.dev/llms-full.txt) is every page in one file, and every page is also available as Markdown at its URL with `.md` instead of `.html`. Point an agent at them before asking it to work on a NiteFx project. ## Next steps - [Tutorial: a full-stack app](https://nitefx.dev/docs/tutorial.md) builds on the SPA template step by step. - [Single-page apps](https://nitefx.dev/docs/guides/single-page-apps.md) and [Islands](https://nitefx.dev/docs/guides/islands.md) cover each model in depth. - [nite.json reference](https://nitefx.dev/docs/reference/nite-json.md) lists every setting. # Tutorial: a full-stack app Build a todo app with a minimal API and a React frontend in one project. Along the way you will use Fast Refresh, C# hot reload that reaches the page, client-side routing, CSS modules, environment variables and a production publish. You need NiteFx installed ([Getting started](https://nitefx.dev/docs/getting-started.md#install-nitefx)). The whole tutorial takes about half an hour. ## 1. Create the project ```bash nite init todos cd todos nite watch ``` The browser opens on the template's page. Leave `nite watch` running for the rest of the tutorial; every step below is a save, not a restart. ## 2. Write the API Replace `Program.cs` with an in-memory store and a few endpoints: ```csharp using System.Collections.Concurrent; using Nite; var builder = WebApplication.CreateBuilder(args); builder.Services.AddNite(); builder.Services.AddSingleton(); var app = builder.Build(); app.UseNite(); var api = app.MapGroup("/api"); api.MapGet("/todos", (TodoStore store) => store.All()); api.MapPost("/todos", (NewTodo input, TodoStore store) => string.IsNullOrWhiteSpace(input.Title) ? Results.ValidationProblem(new Dictionary { ["title"] = ["A title is required."] }) : Results.Created("/api/todos", store.Add(input.Title.Trim()))); api.MapPatch("/todos/{id:int}", (int id, TodoPatch patch, TodoStore store) => store.SetDone(id, patch.Done) is { } todo ? Results.Ok(todo) : Results.NotFound()); app.MapStaticAssets(); app.MapNiteSpa(); app.Run(); record Todo(int Id, string Title, bool Done); record NewTodo(string Title); record TodoPatch(bool Done); class TodoStore { private readonly ConcurrentDictionary _todos = new(); private int _nextId; public TodoStore() { Add("Write the API in C#"); Add("Write the UI in React"); } public IReadOnlyList All() => _todos.Values.OrderBy(t => t.Id).ToList(); public Todo Add(string title) { var todo = new Todo(Interlocked.Increment(ref _nextId), title, false); _todos[todo.Id] = todo; return todo; } public Todo? SetDone(int id, bool done) => _todos.TryGetValue(id, out var todo) ? _todos[id] = todo with { Done = done } : null; } ``` New endpoints and a new service need a restart, so `nite watch` restarts the app. The page waits for the server to come back and reloads (about ten seconds). Check the API directly at `/api/todos`. > [!NOTE] > `app.MapNiteSpa()` serves `Client/index.html` for every path without a file extension, **except** paths under `/api`. A mistyped API URL gets a 404, not a page of HTML. ## 3. A typed API client Create `Client/api.ts`: ```ts // Same origin as the ASP.NET Core app: no CORS, no proxy, no base URL. export type Todo = { id: number; title: string; done: boolean }; async function request(url: string, init?: RequestInit): Promise { const response = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...init?.headers }, }); if (!response.ok) throw new Error(`${init?.method ?? "GET"} ${url}: ${response.status}`); return response.json() as Promise; } export const api = { todos: () => request("/api/todos"), add: (title: string) => request("/api/todos", { method: "POST", body: JSON.stringify({ title }) }), setDone: (id: number, done: boolean) => request(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify({ done }) }), }; /** Runs `callback` when C# hot reload changes the server. Returns the unsubscribe function. */ export function onServerUpdate(callback: () => void) { addEventListener("nite:server-update", callback); return () => removeEventListener("nite:server-update", callback); } ``` Nothing reloads when you save this file: no module on the page imports it yet. ## 4. The todo list Create `Client/TodoList.tsx`: ```tsx import { useEffect, useState, type FormEvent } from "react"; import { api, onServerUpdate, type Todo } from "./api"; export function TodoList() { const [todos, setTodos] = useState([]); const [draft, setDraft] = useState(""); useEffect(() => { const load = () => api.todos().then(setTodos); load(); return onServerUpdate(load); }, []); async function add(event: FormEvent) { event.preventDefault(); if (!draft.trim()) return; const todo = await api.add(draft); setTodos((current) => [...current, todo]); setDraft(""); } async function toggle(todo: Todo) { const updated = await api.setDone(todo.id, !todo.done); setTodos((current) => current.map((t) => (t.id === updated.id ? updated : t))); } return (

Todos

    {todos.map((todo) => (
  • ))}
setDraft(e.target.value)} placeholder="What needs doing?" />
); } ``` And render it from `Client/App.tsx`: ```tsx import { TodoList } from "./TodoList"; export function App() { return (
); } ``` The page updates without a reload. ## 5. Feel Fast Refresh Type something into the "What needs doing?" box, but do not submit it. Now edit `TodoList.tsx`: change `

Todos

` to `

Things to do

` and save. The heading changes in about 150 ms, and your half-typed text is still there. NiteFx found that `TodoList.tsx` exports only components, re-imported just that module, and let React swap the new function in while keeping its state. Edits to `api.ts` work too: NiteFx walks up from the changed module to the components that import it and refreshes those. What resets state, and why: - **Adding or removing a hook** in a component remounts that component (its hook list changed, so old state cannot map onto it). - **Editing `main.tsx`** reloads the page: the entry module has no component boundary above it. ## 6. C# changes reach the page Change the seed data in `TodoStore`'s constructor, or the validation message, and save `Program.cs`. Hot reload applies the change in the running app and NiteFx sends the page a `nite:server-update` event; `TodoList` re-fetches through `onServerUpdate`, and the list updates with nothing reloaded. > [!TIP] > The event never fires in production, so `onServerUpdate` costs nothing there. Use it in any component that shows server data. ## 7. Routing Add React Router: ```bash nite add react-router ``` The running dev server notices `nite.json` changed, pre-bundles the package and reloads the page. Wrap the app in a router in `Client/main.tsx`: ```tsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import { App } from "./App"; createRoot(document.getElementById("root")!).render( , ); ``` Add an about page and routes in `Client/App.tsx`: ```tsx import { NavLink, Route, Routes } from "react-router"; import { TodoList } from "./TodoList"; function About() { return

A todo app with an ASP.NET Core API and a React frontend, built with nite.

; } export function App() { return ( <>
} /> } />
); } ``` Open `/about` directly in a new tab: `MapNiteSpa` serves `index.html` for it, and the router takes over. ## 8. Scoped styles with a CSS module Create `Client/TodoList.module.css`: ```css .list { list-style: none; padding: 0; } .done { color: gray; text-decoration: line-through; } ``` Use it in `TodoList.tsx`: ```tsx import styles from "./TodoList.module.css"; // ...
    {todos.map((todo) => (
  • ``` Class names are scoped to the file, so `.done` here never collides with a `.done` anywhere else. Edit the CSS and save: the components using it refresh in place. Plain `.css` files, Sass, Less, Tailwind and PostCSS work the same way; see [Stylesheets](https://nitefx.dev/docs/guides/stylesheets.md). ## 9. Configuration from the environment Create `.env` in the project folder (next to `nite.json`): ```ini NITE_PUBLIC_APP_NAME=Todos DATABASE_PASSWORD=never-reaches-the-browser ``` Use it in `TodoList.tsx`: ```tsx

    {import.meta.env.NITE_PUBLIC_APP_NAME}

    ``` Only variables starting with `NITE_PUBLIC_` reach client code; the password stays on the server. Editing `.env` reloads the page with the new values. See [Environment variables](https://nitefx.dev/docs/guides/environment.md). ## 10. Publish Stop `nite watch` (Ctrl+C) and publish: ```bash dotnet publish -c Release -o publish cd publish dotnet todos.dll ``` The Release build ran `nite build`: `publish/wwwroot/_nite` holds the minified, content-hashed bundles (with `.br` and `.gz` copies) and `index.html` pointing at them. `MapNiteSpa` serves that built page, and `MapStaticAssets` serves the bundles, compressed with Brotli for browsers that accept it. The published app has no `node_modules`, no esbuild and no dev server. ## What you used | Step | Feature | Read more | |---|---|---| | 2 | A restart for a structural C# change, handled by `nite watch` | [Hot reload](https://nitefx.dev/docs/guides/hot-reload.md) | | 4, 5 | Fast Refresh and the module graph | [How NiteFx works](https://nitefx.dev/docs/concepts/architecture.md) | | 6 | `nite:server-update` | [Browser runtime](https://nitefx.dev/docs/reference/browser-runtime.md) | | 7 | `nite add`, `MapNiteSpa` fallback routing | [Packages](https://nitefx.dev/docs/guides/packages.md), [Single-page apps](https://nitefx.dev/docs/guides/single-page-apps.md) | | 8 | CSS modules | [Stylesheets](https://nitefx.dev/docs/guides/stylesheets.md) | | 9 | `.env` and `envPrefix` | [Environment variables](https://nitefx.dev/docs/guides/environment.md) | | 10 | `nite build` and `MapStaticAssets` | [Deployment](https://nitefx.dev/docs/guides/deployment.md) | # How NiteFx works NiteFx does the orchestration in .NET and hands compilation to esbuild. This page walks through each piece, roughly in the order a request meets them. ```text browser ──GET /_nite/src/App.tsx──▶ ASP.NET Core app │ UseNite(): NiteDevServer │ ├─ transform (esbuild service) ─▶ cached JS │ ├─ rewrite imports (import map, ?import, ?t=) │ └─ Fast Refresh registration browser ◀──────── ESM module ────────┘ browser ◀──WebSocket /_nite/ws── file watcher ─▶ module graph ─▶ update / css / reload ``` ## esbuild, as a service [esbuild](https://esbuild.github.io) compiles TypeScript and JSX. NiteFx downloads the platform's binary from the same npm package esbuild's own installer uses (`@esbuild/linux-x64`, `@esbuild/darwin-arm64` and so on), verifies it against the registry's integrity hash, and caches it per user. It does not run esbuild once per file. It starts one `esbuild --service` process and keeps it for the life of the app, talking esbuild's binary stdio protocol: the same channel esbuild's JavaScript API uses. Transforms take a few milliseconds and run concurrently. Builds can call back into .NET through esbuild's plugin protocol, which is how stylesheets reach the [CSS pipeline](https://nitefx.dev/docs/concepts/architecture.md#the-stylesheet-pipeline). ## Packages without npm A small npm client written in C# reads `dependencies` and `devDependencies` from `nite.json`, resolves semver ranges against the registry, and installs into an ordinary `node_modules` folder, hoisted the way npm does it. Editors, the TypeScript language service and esbuild all find packages where they expect them. - Resolution is recorded in **`nite.lock.json`**. With a current lockfile, an install never asks the registry for metadata. - Tarballs are verified against their integrity hash and cached per user. - **Install scripts never run.** Packages that need a `postinstall` step (native addons, mostly) do not work; NiteFx warns when it resolves one. - Optional dependencies are skipped. See [Packages and the lockfile](https://nitefx.dev/docs/guides/packages.md). ## The dev server `app.UseNite()` mounts the dev server at `/_nite`, inside your app, in the Development environment only. Each source file is compiled on request and cached until it changes. Before serving a module, NiteFx rewrites its import specifiers: | In your code | Served as | Why | |---|---|---| | `import { x } from "./util"` | `./util.ts` | One canonical URL per file, so a module is never instantiated twice. | | `import logo from "./logo.svg"` | `./logo.svg?import` | Non-JavaScript imports become small JavaScript modules (a URL, CSS, JSON). | | `import "react"` | `react` (unchanged) | Bare imports resolve through an **import map** to pre-bundled dependencies. | | A module that just changed | `./Button.tsx?t=1729…` | The timestamp makes importers fetch the new version. | Path aliases from your tsconfig (`@/components/Button`) are resolved the same way. The full URL layout is in [Dev server URLs](https://nitefx.dev/docs/reference/dev-server-urls.md). ## Dependency pre-bundling Browsers can import ES modules, but most packages are not shaped for it: React is still CommonJS, and a package can be hundreds of files. Like Vite, NiteFx pre-bundles every bare import your code uses into a single ESM file per package, in **one** esbuild pass with code splitting, so `react-dom` and your code share one copy of React. CommonJS packages get a generated wrapper whose named exports come from a scan of the package's code (the same approach as Node's `cjs-module-lexer`), so `import { useState } from "react"` works. The result is cached under `.nite/deps` and only rebuilt when your bare imports or the lockfile change. Importing a package for the first time while the app runs triggers a re-bundle and a page reload. ## React Fast Refresh, without Babel esbuild has no Fast Refresh transform, so NiteFx adds one after the fact. It reads esbuild's output for each JSX or TSX module and: 1. finds components by shape: PascalCase functions, arrow functions assigned to PascalCase names, `memo` and `forwardRef` wrappers, and default exports that are components; 2. registers each with `react-refresh`'s runtime and signs it with its hook calls, so a change in hook order is detected; 3. marks the module a **refresh boundary** if every export is a component. A module graph records who imports what among the modules the browser has loaded. When a file changes, NiteFx walks up from it to the nearest boundaries, recompiles, and tells the browser to re-import those modules with a new timestamp. React then swaps the new implementations in and keeps state. If the walk reaches the page's entry module (`main.tsx`), nothing can accept the update, and the page reloads. ## The file watcher and the browser client A file watcher on the client folder debounces saves (editors often write in several steps) and decides, per change: | Change | Message to the browser | |---|---| | A component module, or a module only components import | `update`: re-import the boundaries | | A stylesheet, or a file a stylesheet depends on | `css`: swap the stylesheet in place | | A CSS module | `update` for the components that import it | | `index.html`, the entry module, a deleted file | `reload` | | A compile error | `error`: show the overlay; the next good save clears it | | `nite.json` | Install, re-bundle, `reload` | | `.env*`, `tsconfig*.json` | Recompile, `reload` | The browser client (`/_nite/client.js`) receives these over a WebSocket at `/_nite/ws`. If the socket drops (the server restarting under `dotnet watch`), it reconnects and reloads, waiting until the server answers first so the tab never lands on a "can't connect" page. ## C# hot reload, forwarded NiteFx registers a .NET `MetadataUpdateHandler`. When `dotnet watch` applies a C# change inside the running app, the runtime calls it, and the dev server broadcasts a `server-update` message. The client turns that into a `nite:server-update` DOM event, which pages use to re-fetch data. See [Hot reload](https://nitefx.dev/docs/guides/hot-reload.md). ## The stylesheet pipeline Stylesheets go through the same steps as in Vite, in development and production: 1. **Sass** (`.scss`, `.sass`) or **Less** (`.less`), by extension; 2. **Tailwind CSS 4**, when the file uses it; 3. **PostCSS**, when the project has a config; 4. **esbuild**: `@import` inlining, CSS modules, `url()` assets, and in production, minifying and lowering. Sass runs as the native Dart Sass binary over its embedded protocol, one long-lived compiler like esbuild. PostCSS (with your plugins), Tailwind's compiler and Less are plain JavaScript: NiteFx bundles them from your `node_modules` with esbuild and runs them in [Jint](https://github.com/sebastienros/jint), a JavaScript interpreter written in C#, with a small shim for the Node built-ins they use. Tailwind's file scanner is native code, so NiteFx has its own in C#; it scans the whole project, including Razor views. See [Stylesheets](https://nitefx.dev/docs/guides/stylesheets.md). ## The production build `nite build` (which `dotnet publish` runs for Release builds) does one esbuild build for every entry: islands, HTML pages and the modules listed in `entries`. The output is minified, split into shared chunks, content-hashed and written to `wwwroot/_nite` with a `manifest.json` that maps each entry to its files, CSS and preloads. At runtime in production, `UseNite()` does nothing. The tag helpers and `MapNiteSpa` read the manifest, and `MapStaticAssets` serves the files (precompressed with Brotli and gzip by the static web assets pipeline). See [Development and production](https://nitefx.dev/docs/concepts/development-and-production.md). ## Where things live | Path | What | Commit it? | |---|---|---| | `nite.json` | Packages and settings | Yes | | `nite.lock.json` | Resolved package versions | Yes | | `node_modules/` | Installed packages | No | | `.nite/` | Dev server cache: pre-bundled dependencies, bundled CSS tools | No | | `wwwroot/_nite/` | Production build output | No (built on publish) | | `~/.cache/nite/` | Per-user download cache: tarballs, registry metadata, esbuild, Sass | Not in the project | # Project layout A NiteFx project is an ordinary ASP.NET Core project with a client folder and a `nite.json` next to the project file. ## A single-page app ```text MyApp/ MyApp.csproj AGENTS.md instructions for AI coding agents Program.cs AddNite, UseNite, your API, MapNiteSpa nite.json client packages and settings nite.lock.json resolved package versions .env optional; NITE_PUBLIC_* values reach the client Client/ the client root ("clientRoot" in nite.json) index.html the entry page main.tsx the entry module App.tsx styles.css tsconfig.json wwwroot/ static files served as-is (favicon, robots.txt, images) node_modules/ installed packages .nite/ dev server cache ``` ## Razor Pages with islands ```text MySite/ MySite.csproj AGENTS.md Program.cs AddRazorPages, AddNite, UseNite nite.json nite.lock.json Pages/ _ViewImports.cshtml @addTagHelper *, Nite Shared/_Layout.cshtml inside Index.cshtml Client/ islands/ every module here can be an ("islands" in nite.json) Counter.tsx admin/UserTable.tsx used as lib/ shared code, not islands wwwroot/site.css ``` Both models can live in one project: an `index.html` in the client root and an islands folder work side by side. ## The client root Everything NiteFx compiles lives under the client root, `Client` by default. In development its files are served under `/_nite/src/`, so `Client/App.tsx` is `/_nite/src/App.tsx`. Nothing outside the client root is served by the dev server, with two exceptions: packages from `node_modules` (pre-bundled under `/_nite/deps/`, or as files under `/_nite/npm/`) and the dev runtime itself. Change it with `"clientRoot"` in `nite.json`. Islands are looked up in `"islands"` relative to the client root. ## Files NiteFx reads | File | Read by | Purpose | |---|---|---| | `nite.json` | Dev server, CLI, build | Packages and settings. Reloaded live in development. [Reference](https://nitefx.dev/docs/reference/nite-json.md). | | `nite.lock.json` | Installer | Exact versions. Written by installs; do not edit by hand. | | `Client/tsconfig.json` (and what it `extends` or `references`) | Dev server, build | `compilerOptions.paths` aliases; the build uses the file that declares them. | | `.env`, `.env.local`, `.env.development`, `.env.production`, `.env.*.local` | Dev server, build | Public environment variables. [Guide](https://nitefx.dev/docs/guides/environment.md). | | `postcss.config.*`, `.postcssrc*`, `package.json` `"postcss"` | CSS pipeline | PostCSS plugins. Looked up in the client root, then the project folder. | | `.npmrc` (project and user) | Installer | Registries and credentials. [Guide](https://nitefx.dev/docs/guides/private-registries.md). | | `.gitignore` | Tailwind scanner | Folders to skip when looking for class names. | ## Files NiteFx writes | Path | Contents | In git? | |---|---|---| | `nite.lock.json` | Resolved versions and integrity hashes | **Yes** | | `node_modules/` | Installed packages | No | | `node_modules/.nite-state.json` | What the last install wrote, for cleanup | No | | `.nite/deps/` | Pre-bundled dependencies | No | | `.nite/tools/` | PostCSS, Tailwind and Less bundled for the JavaScript engine | No | | `.nite/css/`, `.nite/runtime/` | Scratch files for stylesheet and production builds | No | | `wwwroot/_nite/` | Production build output | No | The `.gitignore` that `nite init` writes covers all of these: ```text bin/ obj/ node_modules/ .nite/ wwwroot/_nite/ .env*.local ``` ## Build integration The `Nite` package brings MSBuild targets that: - keep `node_modules` and `.nite` out of the project's default items, so the build does not walk thousands of files; - never copy the client sources, `nite.json` or `nite.lock.json` to the build or publish output; - run `nite build` before Release builds and include `wwwroot/_nite` in the publish output; - remove `wwwroot/_nite` on `dotnet clean`. See [MSBuild properties](https://nitefx.dev/docs/reference/msbuild.md). # Development and production The same `Program.cs` runs a compiling dev server in development and serves prebuilt static files in production. This page explains the switch and the differences. ## Which mode am I in? `AddNite()` picks the mode from the hosting environment: **development** when `IWebHostEnvironment.IsDevelopment()` is true (`ASPNETCORE_ENVIRONMENT=Development`), **production** otherwise. Override it with [Nite.NiteOptions](https://nitefx.dev/api/Nite.NiteOptions.html): ```csharp builder.Services.AddNite(options => { // Serve the production build even in Development, to check it locally. options.Mode = NiteMode.Production; }); ``` `NiteMode.Development` does the opposite: the dev server in a Staging environment, say. ## What changes | | Development | Production | |---|---|---| | `UseNite()` | Mounts the dev server at `/_nite` and starts the file watcher | Does nothing | | Source files | Compiled on request, cached until they change | Built ahead of time by `nite build` | | Packages | Installed on startup from `nite.lock.json`; pre-bundled into `.nite/deps` | Bundled into the output chunks | | `MapNiteSpa()` | Serves `Client/index.html` from source, with the dev client injected | Serves `wwwroot/_nite/index.html` with hashed URLs | | ``, ``, `` | Point at `/_nite/src/...` and the import map | Point at hashed files from `manifest.json` | | CSS imported from code | Injected as `