Our Sitefinity renderer project was working perfectly. No incidents, no complaints, no reason to touch it.

But it had been a few months since we'd last reviewed the frontend build, and npm package pollution attacks have only gotten more frequent in that time. Compromised packages, typosquats, dependency confusion attacks landing in widely-used libraries. "It works" stopped feeling like enough of a reason to leave it alone.

So we modernised the build: fifteen deliberate decisions that made it leaner, safer and easier for the team to actually work in.

Fewer moving parts, fewer things to trust

A smaller toolchain means fewer build quirks to remember, fewer changelogs to track, and fewer "wait, why did this suddenly break" moments when someone bumps a transitive dependency three levels down. It also shrinks the attack surface, which is exactly what prompted this review.

1. Cut the toolchain down to four things that matter.

Why: every extra package is another dependency that can be compromised, and package sprawl had turned routine audits into a weekly chore.

How: consolidated the stack to Vite, TypeScript, Lightning CSS and Sass, nothing else earns a place in package.json.

2. Made Vite the single source of truth for compilation.

Why: MSBuild and Vite were both trying to compile TypeScript. That produced inconsistent output and conflicts nobody could fully explain.

How: TypeScriptCompileBlocked=true in the .csproj hands compilation entirely to Vite, while tsconfig.json still drives editor intelligence and type checking.

3. Moved CSS transforms to Lightning CSS.

Why: we needed fast, consistent minification without bolting on another heavyweight dependency.

How: configured directly in vite.config.ts, driven by real Browserslist targets instead of guesswork.

Getting Visual Studio and VS Code to agree with each other

Today's frontend work rarely happens in one place. You're updating a C# controller in Visual Studio in the morning, tweaking a design token in VS Code after lunch, and letting your AI CLI rewrite a chunk of TypeScript before the end of the day. If formatting and linting don't agree across those tools, every handoff becomes friction: mismatched indentation, inconsistent quotes, code review comments about style. We wanted Visual Studio, VS Code and everything in between to produce the same output automatically, on every machine, for every developer.

4. Standardised formatting with Prettier, aligned to .editorconfig.

Why: Visual Studio users and VS Code/CLI users were quietly producing different formatting, and that inconsistency showed up as noise in every diff.

How: .editorconfig sets the shared editor baseline, Prettier enforces deterministic formatting on top for .ts, .js, .scss, .css and .json.

5. Turned on format-on-save.

Why: whitespace and indentation-only changes were cluttering pull requests and slowing reviews down for no real benefit.

How: editor.formatOnSave enabled at the workspace level, so formatting happens before a diff is even generated.

6. Automated save-time hygiene.

Why: import cleanup and lint fixes were easy to forget under deadline pressure, and inconsistent enough that nobody could rely on them being done.

How: editor.codeActionsOnSave configured for import organisation and ESLint auto-fixes on every save.

Formatting keeps code looking the same. It doesn't stop someone shipping a broken type or an invalid selector. That's a different, stricter layer.

7. Locked down quality gates with ESLint, Stylelint and tsc --noEmit.

Why: formatting alone doesn't catch broken types, bad selectors or common JS mistakes, we needed enforced correctness alongside tidy formatting.

How: all three run through one command, npm run lint, so nothing gets skipped by accident.

Removing the guesswork from "which version broke this"

Pinned versions and reproducible installs remove an entire category of debugging. When Node, npm and every dependency are locked down, a security scare turns into a five-minute lockfile check instead of a scavenger hunt across three developer machines running three different setups.

8. Pinned Node and npm to exact versions.

Why: version drift between developer machines and CI is exactly how "works on my machine" bugs and inconsistent security exposure creep in.

How: Node 24.18.0 and npm 11.18.0 pinned via .nvmrc, engines and packageManager in package.json.

9. Enforced engine-strict and save-exact in .npmrc.

Why: floating version ranges are precisely how supply chain attacks slip in through automatic minor or patch bumps.

How: .npmrc sets engine-strict=true and save-exact=true, so installs fail fast on the wrong runtime and dependencies never silently drift.

10. Made npm ci the default install path.

Why: a plain npm install can quietly rewrite the lockfile and pull in dependency versions nobody reviewed.

How: npm ci is now standard across every developer machine and CI runner, installing exactly what the lockfile says and nothing else.

One build, not two

The frontend build used to live outside the .NET world entirely: a separate terminal, a separate mental model, easy to forget after a pull. Folding it into the normal dotnet build and Visual Studio flow means developers don't have to context-switch just to get current assets. When something is missing, it shows up early, in the build, with a message that tells you what to do next.

11. Folded the frontend build into dotnet build.

Why: a separate manual build step is a step that eventually gets forgotten, and stale assets in production are a painful way to find out.

How: a BuildEdxAssets MSBuild target runs npm run visualstudio:$(Configuration) automatically as part of the normal build.

12. Added an early, actionable failure for missing tooling.

Why: a missing Node or npm install used to surface as a confusing downstream error, costing new developers hours during onboarding.

How: EnsureNodeAndNpmForEdxBuild checks for both up front and fails immediately with clear setup guidance.

13. Fixed target ordering so assets never go missing from publish.

Why: "it builds fine locally but the assets are missing in production" is a classic, expensive-to-debug failure mode.

How: IncludeEdxDistAssets re-includes assets/dist/** as Content before AssignTargetPaths, so generated files are guaranteed to ship.

14. Skipped frontend steps on design-time builds.

Why: IDE tooling triggering a full npm build just to render IntelliSense was slowing the everyday dev loop down for no reason.

How: frontend npm steps are guarded with '$(DesignTimeBuild)' != 'true'.

One command before you open a PR

All of the above only helps if a developer can check it in ten seconds instead of hoping for the best.

15. Built a one-command verify step with real reporting.

Why: we wanted one pre-PR gate that catches formatting, lint and build issues before a reviewer does, without drowning developers in noise.

How: npm run verify runs format, lint and a production build, then generates both an HTML and JSON report and opens the HTML version automatically. Warning policy is deliberate too: unresolved asset warnings surface clearly, while known-noisy signals such as binary asset compression behaviour are treated as informational.

This was a review, not a rewrite: fifteen decisions, most of them small, applied consistently across the stack. The payoff shows up on a new developer's first day, and on every ordinary day after that.

If your Sitefinity renderer hasn't had a build review in a while, that's usually less about the code and more about the moment. Happy to compare notes if you're due for one.

sitefinity, continuous innovation

 


 

Explore other related latest insights, videos and news