BunPackage: npm Packages via .csprojeQuantic.UI allows declaring npm package dependencies directly in .csproj files, keeping the developer experience 100% .NET with zero package.json, zero npm, and zero Node.js.
<BunPackage Include="dayjs" Version="1.11.13" />
That's it. On dotnet build, the SDK automatically:
1.
Generates a temporary package.json in obj/eQuantic/npm/
2.
Installs packages using the embedded Bun runtime
3.
Creates a node_modules symlink for tool resolution
4.
Skips installation on incremental builds
├── 1. ResolveBunPath ← Extracts embedded Bun from NuGet package
├── 2. InstallBunPackages ← NEW: Installs <BunPackage> items
│ ├── Creates obj/eQuantic/npm/package.json
│ ├── Runs: bun add <package>@<version>
│ └── Symlinks: node_modules → obj/eQuantic/npm/node_modules/
├── 3. CompileEQuanticUI ← C# → TypeScript → JavaScript
├── 4. CopyEQuanticRuntime ← Copies runtime.js
├── MyApp.csproj ← BunPackage declared here
├── node_modules/ ← Symlink (gitignored)
│ └── → obj/eQuantic/npm/node_modules/
│ └── npm/ ← Hidden from developer
│ ├── package.json ← Auto-generated
│ ├── bun.lock ← Auto-generated
│ └── node_modules/ ← Real packages
The developer never sees or touches package.json. The node_modules symlink is gitignored and auto-created.
Multiple BunPackage items are supported. Each runs as a separate bun add:
<BunPackage Include="dayjs" Version="1.11.13" />
<BunPackage Include="marked" Version="15.0.6" />
Works equally well with CSS-only packages imported via @import:
<BunPackage Include="tw-animate-css" Version="1.2.5" />
@import "tw-animate-css";
The InstallBunPackages target lives in Sdk.targets:
<Target Name="InstallBunPackages"
AfterTargets="ResolveBunPath"
BeforeTargets="CompileEQuanticUI"
Condition="'@(BunPackage)' != '' And '$(BunPath)' != ''">
Incremental
Skips if obj/eQuantic/npm/node_modules/ already exists
Clean build
Runs bun add for each package on first build or after dotnet clean
Cross-platform
Symlink on macOS/Linux, mklink /D on Windows
Isolation
Packages live in obj/, not project root
No pollution
No package.json, bun.lockb, or node_modules committed to git
To force a fresh install (e.g., after changing versions):
# Option 1: Delete the npm cache
Why not generate package.json at project root?
•
Pollutes the .NET project with npm artifacts
•
Developers might accidentally commit it
•
Confuses IDEs into treating it as a Node.js project
•
Violates the "100% .NET" principle
Why symlink instead of NODE_PATH?
•
Symlinks are universally supported by the tools that resolve node_modules
•
NODE_PATH has inconsistent behavior across tools
•
The symlink is transparent: if a tool looks for node_modules/, it finds them
Why bun add instead of bun install?
•
bun add works with a minimal {"private": true} package.json
•
MSBuild item batching (%(BunPackage.Identity)) naturally maps to individual bun add calls
•
Each package is installed with its exact version
Why not use bun x --install?
•
bun x is for running CLI tools, not for installing libraries
•
Packages referenced in CSS (@plugin, @import) need to exist in node_modules/
•
bun add is the correct semantic for "install this library"
Approach
package.json needed
npm/Node.js needed
Developer experience
Traditional npm
Yes
Yes
Must manage two ecosystems
BunPackage
No
No
Pure .NET, zero friction
Packages not updating after version change
Cause: node_modules already exists, so InstallBunPackages skips
Fix:
Symlink permission error on Windows
Cause: Creating symlinks on Windows requires elevated permissions or Developer Mode
Fix: Enable Developer Mode in Windows Settings, or run terminal as Administrator
•
Build Flow: Complete MSBuild target execution order