Svelte 5 islands for ASP.NET (Razor Pages/MVC) with typed props, SvelteKit-style forms and remote functions, opt-in Jint/Node.js/Bun/custom SSR, and a Vite plugin. Monorepo: NuGet packages in src/, the sveltenet npm package in packages/sveltenet, samples in samples/ (one concept each — see samples/README.md), docs in docs/.
dotnet test SvelteNet.slnx # all .NET tests (integration tests boot the samples)
cd packages/sveltenet && bun run test # JS tests (node --test; runes modules are NOT node-testable)
bun install # workspace install (bun.lock committed; npm/pnpm also work)
cd samples/<Sample> && bun run build # ONE vite build produces client + SSR bundles
cd samples/<Sample> && dotnet run # launchSettings pin Development; production: bun run build,
# then ASPNETCORE_ENVIRONMENT=Production dotnet run --no-launch-profile
bun run docs:build # VitePress docs site (config in docs/.vitepress/)docs/*.md is the user-facing documentation — it is also built into a VitePress site (docs/.vitepress/config.mts; new pages must be added to its sidebar) and must stay in sync with the code. When you change public API surface, generated file shapes, scaffolded templates, options, or protocols, update the matching doc in the same change:
docs/getting-started.md— setup flow, scaffolded file listdocs/dev-mode.md— dev/HMR behavior,dotnet watch+ vite workflowdocs/forms.md— SveltePage forms,data.problemvalidation errors, antiforgery,enhance()docs/mvc.md—[SvelteComponent],@Html.Svelte, duplicate-render rulesdocs/remote-functions.md—[Query]/[Command]/[Form], client API, generator, SSR/hydrationdocs/options.md—SvelteOptions↔ vite plugin option pairing, discovery scope, SSR engineREADME.md— overview + doc links only; details belong in docs/packages/sveltenet/README.md— npm-facing plugin/runtime docsROADMAP.md— move items to Shipped when they land; add/adjust items when plans change. Completing a roadmap item without updating this file is an incomplete change.
Code samples in docs must compile against the current API — treat a doc snippet like a test that isn't executed. If behavior in a doc no longer matches reality, fixing the doc is part of the task, not a follow-up.
- SvelteNet.Core stays host-agnostic — no MVC/Razor/HTTP dependencies (Blazor hosting is planned). New transports go in
SvelteNet.AspNetCoreas minimal-API endpoints, not MVC filters. Core also carries no JS engine: Jint lives inSvelteNet.Jint. - Everything ships
net8.0;net10.0. Guard .NET 9+ APIs with#if NET9_0_OR_GREATER;SvelteNet.Buildtargets net8.0 withRollForward=LatestMajorso one copy of the tool runs under any SDK. - One serialization contract:
SvelteJson.Options(camelCase props, dict keys, enums;long/ulong/decimalas strings, because a JS number cannot hold them). All three TS emitters —SvelteMetadataGenerator,SvelteRemoteGenerator, and reflectionTypeGen/— must always match it. A TS type that disagrees with the JSON is a bug. - SSR is asynchronous:
ISvelteSsrEngine.RenderAsync/ISvelteSsrFetchHandler.HandleAsync. A render can await remote queries, so no layer may block on it. A synchronous JS runtime moves the render to a bounded worker (seeJintSsrEngine) rather than blocking the request thread.SveltePageprerenders in the page filter so.cshtmlstays synchronous; MVC uses@await Html.SvelteAsync(...). - Authorization is descriptor-level:
[Authorize]/[AllowAnonymous]becomeRemoteAuthorizationdata on the descriptor, whichMapSvelteRemoteprojects onto endpoints ANDRemoteSsrFetchHandlerevaluates directly. The bridge bypasses routing, so anything that only guards endpoints leaves SSR open. Generated and reflection paths must agree. - TypeScript names are resolved in two stages: emitters produce a namespace-qualified placeholder (
TypeScriptNaming.Placeholder); the scaffolder — the only component that sees every assembly — shortens and substitutes. Never emit a final type name from a generator. - Validation errors are RFC 9457 problem details (400,
application/problem+json, ASP.NETerrorsmember) everywhere: remote endpoints viaResults.ValidationProblem, the SSR fetch bridge, and enhanced SveltePage posts (whosedataextension member carries fresh props; SSR props expose the same shape asdata.problem).SvelteValidationExceptionis the throwing API. Never invent a bespoke error shape. - BYOV validation pipeline: dispatchers (generated AND reflection — both must emit the same
await args.ValidateBoundAsync()between binding and invocation) run the registeredISvelteRemoteValidators overRemoteArguments.Bound;DataAnnotationsRemoteValidatoris registered by default. Pages get the equivalent throughModelState. New validation sources plug in via DI, never via new wire shapes or dispatcher special cases. - Paths contract: manifest keys are
{PagesPath}/{Component}.svelte;SvelteOptions(C#) andsveltenet()vite plugin options must agree (PagesPath/pagesPath,ClientOutput/clientOutDir,ServerOutput/serverOutDir). The SSR bundle is deliberately NOT under wwwroot. - Remote dispatch AND registration are descriptor-based:
SvelteNet.Generatorsemits compiled dispatchers (module initializer →SvelteRemoteDescriptors);AddSvelteNetconsumes registered descriptors scoped to the app's assemblies — no runtime reflection scan. Reflection (FromReflection+ theTypeDiscoveryfallback) only covers apps without the analyzer and must stay behaviorally identical to generated code. The scaffolder generates one colocated*.remote.tsclass per service from the same descriptors. - Discovery is assembly-scoped: multiple SvelteNet apps share the test process, so
[SvelteRemote]/[SvelteComponent]/SveltePagediscovery must respectSvelteOptions.ApplicationAssemblies(defaults to theAddSvelteNetcaller). An unscoped scan leaks one sample's services into another's container. - Type generation is build-time:
SvelteNet.Build.targets(imported by each sample, shipped in the NuGet's build/ later) runsSvelteNet.Buildafter everydotnet build— it loads the built app assembly in an isolated ALC and invokes the scaffolder via reflection (deliberately no SvelteNet project references, so the app's copy is the only copy). Startup scaffolding (EnableScaffolding = true) is only a fallback for apps without the targets. - Scaffolder rules:
.svelte-net/types/**/*.d.tsand colocated*.remote.tsclasses are regenerated every build ("do not edit" headers);.sveltecomponents,vite.config.ts,tsconfig.json, andpackage.jsonare write-once and user-owned after creation. Only.svelte-netmay be deleted from — stale clients are tracked in.svelte-net/generated-clients.txt, never found by scanning file contents.SvelteNetGenerateTypesis incremental; keep itsInputs/Outputs. Client/SSR entry modules come from the npm package, so apps do not scaffoldmount.tsorrender.ts. Each sample commitswwwroot/.gitkeep— Development crashes without the directory. - SSR queries work: awaited queries resolve during SSR through the in-process fetch bridge (
ISvelteSsrFetchHandler) and stash into the head viahydratable;RemoteQueryinstances are per-render on the server (pooled engines keep module state alive).then/catch/finallyon queries are property getters — the reactive read must happen during the synchronous.thenproperty access. Jint has no Node APIs beyond the served shims (node:async_hooks); Vite SSR builds bundle everything (noExternal) because production has no node_modules. - SSR is opt-in:
AddSvelteNet()is client-only; choose exactly one ofAddJintSSR(),AddNodeSSR(),AddBunJsSSR(), orAddCustomRenderer(...). Backend-specific settings stay inJintSsrOptions,NodeSsrOptions, orBunJsSsrOptions, notSvelteOptions. Node.js/Bun require their configured CLI on PATH; Jint's remote-query bridge is in-process, while CLI engines use authenticated loopback fetches against a server-reported or explicitly trustedBaseUrl— never derive that target from the incomingHostheader. preserveEntrySignaturesis load-bearing in the vite plugin — without it Rollup/Rolldown treeshakes entry exports and hydration silently breaks.
- Tabs for indentation (C#, JS, Svelte). Svelte 5 runes only — no legacy syntax; follow the svelte:svelte-core-bestpractices skill for any .svelte work.
- New features need tests at the right layer: TypeGen/renderer →
SvelteNet.Core.Tests; scaffolder/page/endpoints →SvelteNet.AspNetCore.Tests(integration factories boot the samples in dev mode with scaffolding disabled:TodoAppFactoryfor forms/enhance,RemoteFunctionsFactoryfor remote functions,MvcHelloFactoryfor MVC); transport helpers →packages/sveltenet/*.test.mjs. - The samples double as integration-test hosts AND living demos — new features should appear in the sample that owns the concept (
TodoAppforms/props,RemoteFunctionsremote+async,MvcHelloMVC). - Generated
remote.tsinsamples/RemoteFunctionsis committed;dotnet buildregenerates it after changingTodoApior the generator output shape.