Skip to content

Latest commit

 

History

History
57 lines (46 loc) · 9.44 KB

File metadata and controls

57 lines (46 loc) · 9.44 KB

SvelteNet

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/.

Commands

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 are part of every change

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 list
  • docs/dev-mode.md — dev/HMR behavior, dotnet watch + vite workflow
  • docs/forms.md — SveltePage forms, data.problem validation errors, antiforgery, enhance()
  • docs/mvc.md[SvelteComponent], @Html.Svelte, duplicate-render rules
  • docs/remote-functions.md[Query]/[Command]/[Form], client API, generator, SSR/hydration
  • docs/options.mdSvelteOptions ↔ vite plugin option pairing, discovery scope, SSR engine
  • README.md — overview + doc links only; details belong in docs/
  • packages/sveltenet/README.md — npm-facing plugin/runtime docs
  • ROADMAP.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.

Architecture invariants

  • SvelteNet.Core stays host-agnostic — no MVC/Razor/HTTP dependencies (Blazor hosting is planned). New transports go in SvelteNet.AspNetCore as minimal-API endpoints, not MVC filters. Core also carries no JS engine: Jint lives in SvelteNet.Jint.
  • Everything ships net8.0;net10.0. Guard .NET 9+ APIs with #if NET9_0_OR_GREATER; SvelteNet.Build targets net8.0 with RollForward=LatestMajor so one copy of the tool runs under any SDK.
  • One serialization contract: SvelteJson.Options (camelCase props, dict keys, enums; long/ulong/decimal as strings, because a JS number cannot hold them). All three TS emitters — SvelteMetadataGenerator, SvelteRemoteGenerator, and reflection TypeGen/ — 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 (see JintSsrEngine) rather than blocking the request thread. SveltePage prerenders in the page filter so .cshtml stays synchronous; MVC uses @await Html.SvelteAsync(...).
  • Authorization is descriptor-level: [Authorize]/[AllowAnonymous] become RemoteAuthorization data on the descriptor, which MapSvelteRemote projects onto endpoints AND RemoteSsrFetchHandler evaluates 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.NET errors member) everywhere: remote endpoints via Results.ValidationProblem, the SSR fetch bridge, and enhanced SveltePage posts (whose data extension member carries fresh props; SSR props expose the same shape as data.problem). SvelteValidationException is 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 registered ISvelteRemoteValidators over RemoteArguments.Bound; DataAnnotationsRemoteValidator is registered by default. Pages get the equivalent through ModelState. 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#) and sveltenet() 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.Generators emits compiled dispatchers (module initializer → SvelteRemoteDescriptors); AddSvelteNet consumes registered descriptors scoped to the app's assemblies — no runtime reflection scan. Reflection (FromReflection + the TypeDiscovery fallback) only covers apps without the analyzer and must stay behaviorally identical to generated code. The scaffolder generates one colocated *.remote.ts class per service from the same descriptors.
  • Discovery is assembly-scoped: multiple SvelteNet apps share the test process, so [SvelteRemote]/[SvelteComponent]/SveltePage discovery must respect SvelteOptions.ApplicationAssemblies (defaults to the AddSvelteNet caller). 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) runs SvelteNet.Build after every dotnet 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.ts and colocated *.remote.ts classes are regenerated every build ("do not edit" headers); .svelte components, vite.config.ts, tsconfig.json, and package.json are write-once and user-owned after creation. Only .svelte-net may be deleted from — stale clients are tracked in .svelte-net/generated-clients.txt, never found by scanning file contents. SvelteNetGenerateTypes is incremental; keep its Inputs/Outputs. Client/SSR entry modules come from the npm package, so apps do not scaffold mount.ts or render.ts. Each sample commits wwwroot/.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 via hydratable; RemoteQuery instances are per-render on the server (pooled engines keep module state alive). then/catch/finally on queries are property getters — the reactive read must happen during the synchronous .then property 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 of AddJintSSR(), AddNodeSSR(), AddBunJsSSR(), or AddCustomRenderer(...). Backend-specific settings stay in JintSsrOptions, NodeSsrOptions, or BunJsSsrOptions, not SvelteOptions. 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 trusted BaseUrl — never derive that target from the incoming Host header.
  • preserveEntrySignatures is load-bearing in the vite plugin — without it Rollup/Rolldown treeshakes entry exports and hydration silently breaks.

Conventions

  • 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: TodoAppFactory for forms/enhance, RemoteFunctionsFactory for remote functions, MvcHelloFactory for 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 (TodoApp forms/props, RemoteFunctions remote+async, MvcHello MVC).
  • Generated remote.ts in samples/RemoteFunctions is committed; dotnet build regenerates it after changing TodoApi or the generator output shape.